commit
ba8abdb8d3
29 changed files with 1432 additions and 222 deletions
|
@ -1,5 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
--c-primary: #2c3333;
|
--c-primary: #2c3333;
|
||||||
|
--c-disabled: rgb(81, 81, 81);
|
||||||
--c-hover: hsl(180, 7%, 30%);
|
--c-hover: hsl(180, 7%, 30%);
|
||||||
--c-active: hsl(180, 7%, 25%);
|
--c-active: hsl(180, 7%, 25%);
|
||||||
--c-primary-accent: hsl(180, 7%, 40%);
|
--c-primary-accent: hsl(180, 7%, 40%);
|
||||||
|
|
|
@ -71,6 +71,11 @@
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.icon > .icon-scroll {
|
||||||
|
-webkit-mask-image: url("../res/icons/scroll.svg");
|
||||||
|
mask-image: url("../res/icons/scroll.svg");
|
||||||
|
}
|
||||||
|
|
||||||
.ui.icon > .icon-settings {
|
.ui.icon > .icon-settings {
|
||||||
-webkit-mask-image: url("../res/icons/settings.svg");
|
-webkit-mask-image: url("../res/icons/settings.svg");
|
||||||
mask-image: url("../res/icons/settings.svg");
|
mask-image: url("../res/icons/settings.svg");
|
||||||
|
@ -100,6 +105,39 @@
|
||||||
mask-image: url("../res/icons/paintbrush.svg");
|
mask-image: url("../res/icons/paintbrush.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.inline-icon.icon-save::after,
|
||||||
|
.ui.icon > .icon-save {
|
||||||
|
-webkit-mask-image: url("../res/icons/save.svg");
|
||||||
|
mask-image: url("../res/icons/save.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.inline-icon.icon-pencil::after,
|
||||||
|
.ui.icon > .icon-pencil {
|
||||||
|
-webkit-mask-image: url("../res/icons/pencil.svg");
|
||||||
|
mask-image: url("../res/icons/pencil.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.inline-icon.icon-download::after,
|
||||||
|
.ui.icon > .icon-download {
|
||||||
|
-webkit-mask-image: url("../res/icons/download.svg");
|
||||||
|
mask-image: url("../res/icons/download.svg");
|
||||||
|
}
|
||||||
|
.ui.inline-icon.icon-upload::after,
|
||||||
|
.ui.icon > .icon-upload {
|
||||||
|
-webkit-mask-image: url("../res/icons/upload.svg");
|
||||||
|
mask-image: url("../res/icons/upload.svg");
|
||||||
|
}
|
||||||
|
.ui.inline-icon.icon-more-horizontal::after,
|
||||||
|
.ui.icon > .icon-more-horizontal {
|
||||||
|
-webkit-mask-image: url("../res/icons/more-horizontal.svg");
|
||||||
|
mask-image: url("../res/icons/more-horizontal.svg");
|
||||||
|
}
|
||||||
|
.ui.inline-icon.icon-trash::after,
|
||||||
|
.ui.icon > .icon-trash {
|
||||||
|
-webkit-mask-image: url("../res/icons/trash.svg");
|
||||||
|
mask-image: url("../res/icons/trash.svg");
|
||||||
|
}
|
||||||
|
|
||||||
.ui.inline-icon.icon-expand::after,
|
.ui.inline-icon.icon-expand::after,
|
||||||
.ui.icon > .icon-expand {
|
.ui.icon > .icon-expand {
|
||||||
-webkit-mask-image: url("../res/icons/expand.svg");
|
-webkit-mask-image: url("../res/icons/expand.svg");
|
||||||
|
|
|
@ -227,11 +227,25 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-field-wrapper input {
|
.host-field-wrapper > .host-field {
|
||||||
flex-shrink: 0;
|
display: flex;
|
||||||
width: calc(100% - 15px);
|
width: calc(100% - 15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-field-wrapper > .host-field > .label {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
|
||||||
|
background-color: var(--c-primary);
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-field-wrapper input {
|
||||||
display: block;
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
59
css/ui/workspace.css
Normal file
59
css/ui/workspace.css
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#workspace-select input.autocomplete-text {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
|
padding-left: 5px;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspace-select-area .buttons > *:last-child {
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
|
||||||
|
background-color: var(--c-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-btn:disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: var(--c-disabled) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-btn:hover {
|
||||||
|
background-color: var(--c-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-btn:active {
|
||||||
|
background-color: var(--c-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-collapsible {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-collapsible > *:first-child {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
height: 21px;
|
||||||
|
|
||||||
|
transition-duration: 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-collapsible.collapsed > *:first-child {
|
||||||
|
width: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
108
index.html
108
index.html
|
@ -4,14 +4,15 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>openOutpaint 🐠</title>
|
<title>openOutpaint 🐠</title>
|
||||||
<!-- CSS Variables -->
|
<!-- CSS Variables -->
|
||||||
<link href="css/colors.css?v=3f81e80" rel="stylesheet" />
|
<link href="css/colors.css?v=f732f19" rel="stylesheet" />
|
||||||
<link href="css/icons.css?v=9ae0466" rel="stylesheet" />
|
<link href="css/icons.css?v=466e14e" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="css/index.css?v=882f400" rel="stylesheet" />
|
<link href="css/index.css?v=61e08f5" rel="stylesheet" />
|
||||||
<link href="css/layers.css?v=92c0352" rel="stylesheet" />
|
<link href="css/layers.css?v=92c0352" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="css/ui/generic.css?v=30837f8" rel="stylesheet" />
|
<link href="css/ui/generic.css?v=30837f8" rel="stylesheet" />
|
||||||
|
|
||||||
|
<link href="css/ui/workspace.css?v=2a9fdf7" rel="stylesheet" />
|
||||||
<link href="css/ui/history.css?v=0b03861" rel="stylesheet" />
|
<link href="css/ui/history.css?v=0b03861" rel="stylesheet" />
|
||||||
<link href="css/ui/layers.css?v=1d66c2b" rel="stylesheet" />
|
<link href="css/ui/layers.css?v=1d66c2b" rel="stylesheet" />
|
||||||
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
||||||
|
@ -38,17 +39,57 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="info" class="menu-container" style="min-width: 200px">
|
<div id="info" class="menu-container" style="min-width: 200px">
|
||||||
<label>
|
<div
|
||||||
Host
|
id="workspace-select-area"
|
||||||
<div class="host-field-wrapper">
|
style="display: flex; margin-bottom: 5px">
|
||||||
<input id="host" value="http://127.0.0.1:7860" />
|
<div id="workspace-select"></div>
|
||||||
|
<div class="buttons" style="display: flex">
|
||||||
|
<button
|
||||||
|
id="save-workspace-btn"
|
||||||
|
class="ui inline-icon icon-save workspace-btn"
|
||||||
|
title="Save Workspace"></button>
|
||||||
|
<button
|
||||||
|
id="rename-workspace-btn"
|
||||||
|
class="ui inline-icon icon-pencil workspace-btn"
|
||||||
|
title="Rename Workspace"></button>
|
||||||
|
<button
|
||||||
|
id="more-workspace-btn"
|
||||||
|
class="ui inline-icon icon-more-horizontal workspace-btn"
|
||||||
|
title="More Options"></button>
|
||||||
<div
|
<div
|
||||||
id="connection-status-indicator"
|
id="more-workspace-menu"
|
||||||
class="connection-status before">
|
class="workspace-collapsible collapsed">
|
||||||
<span id="connection-status-indicator-text">Waiting</span>
|
<div>
|
||||||
|
<button
|
||||||
|
id="export-workspace-btn"
|
||||||
|
class="ui inline-icon icon-download workspace-btn"
|
||||||
|
title="Export Workspace"></button>
|
||||||
|
<button
|
||||||
|
id="import-workspace-btn"
|
||||||
|
class="ui inline-icon icon-upload workspace-btn"
|
||||||
|
title="Import Workspace"></button>
|
||||||
|
<button
|
||||||
|
id="delete-workspace-btn"
|
||||||
|
class="ui inline-icon icon-trash workspace-btn"
|
||||||
|
title="Delete Workspace"></button>
|
||||||
|
</div>
|
||||||
|
<div style="width: 5px; background-color: var(--c-primary)"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="width: 5px; background-color: var(--c-primary)"></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
|
<div class="host-field-wrapper">
|
||||||
|
<div class="host-field">
|
||||||
|
<div class="label">Host</div>
|
||||||
|
<input id="host" value="http://127.0.0.1:7860" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="connection-status-indicator"
|
||||||
|
class="connection-status before">
|
||||||
|
<span id="connection-status-indicator-text">Waiting</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Prompts section -->
|
<!-- Prompts section -->
|
||||||
<button type="button" class="collapsible">Prompts</button>
|
<button type="button" class="collapsible">Prompts</button>
|
||||||
|
@ -88,7 +129,7 @@
|
||||||
<button id="refreshModelsBtn" onclick="getModels(true)">
|
<button id="refreshModelsBtn" onclick="getModels(true)">
|
||||||
<img
|
<img
|
||||||
class="refreshbutton"
|
class="refreshbutton"
|
||||||
src="./res/icons/refresh-cw.svg"
|
src="./res/icons/refresh-cw.svg?v=f627140"
|
||||||
alt="refresh models"
|
alt="refresh models"
|
||||||
title="refresh models" />
|
title="refresh models" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -100,7 +141,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
id="seed"
|
id="seed"
|
||||||
onchange="changeSeed()"
|
onchange="changeSeed()"
|
||||||
min="1"
|
min="-1"
|
||||||
max="9999999999"
|
max="9999999999"
|
||||||
value="-1"
|
value="-1"
|
||||||
step="1" />
|
step="1" />
|
||||||
|
@ -242,7 +283,16 @@
|
||||||
|
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
<div id="ui-history" class="floating-window" style="right: 10px; top: 10px">
|
<div id="ui-history" class="floating-window" style="right: 10px; top: 10px">
|
||||||
<div class="draggable floating-window-title">History</div>
|
<div class="draggable floating-window-title">
|
||||||
|
History
|
||||||
|
<div style="flex: 1"></div>
|
||||||
|
<button
|
||||||
|
id="history-logs-btn"
|
||||||
|
class="ui icon header-button"
|
||||||
|
title="Generate History Log">
|
||||||
|
<div class="icon-scroll"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="menu-container" style="min-width: 200px">
|
<div class="menu-container" style="min-width: 200px">
|
||||||
<div id="history" class="history"></div>
|
<div id="history" class="history"></div>
|
||||||
<div class="button-array" style="padding: 10px">
|
<div class="button-array" style="padding: 10px">
|
||||||
|
@ -340,22 +390,27 @@
|
||||||
<div class="ui separator"></div>
|
<div class="ui separator"></div>
|
||||||
<iframe
|
<iframe
|
||||||
id="page-overlay"
|
id="page-overlay"
|
||||||
src="pages/configuration.html?v=7fca00b"></iframe>
|
src="pages/configuration.html?v=fdbd833"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Basics -->
|
<!-- Basics -->
|
||||||
<script src="js/global.js?v=ac30d16" type="text/javascript"></script>
|
<script src="js/global.js?v=ac30d16" type="text/javascript"></script>
|
||||||
|
<script src="js/defaults.js?v=5b06818" type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Base Libs -->
|
<!-- Base Libs -->
|
||||||
<script src="js/lib/util.js?v=e82dd04" type="text/javascript"></script>
|
<script src="js/lib/util.js?v=e82dd04" type="text/javascript"></script>
|
||||||
<script src="js/lib/events.js?v=2ab7933" type="text/javascript"></script>
|
<script src="js/lib/events.js?v=2ab7933" type="text/javascript"></script>
|
||||||
|
<script
|
||||||
|
src="js/lib/workspaces.js?v=4fbd55b"
|
||||||
|
type="text/javascript"></script>
|
||||||
|
<script src="js/lib/db.js?v=434363b" type="text/javascript"></script>
|
||||||
<script src="js/lib/input.js?v=aa14afc" type="text/javascript"></script>
|
<script src="js/lib/input.js?v=aa14afc" type="text/javascript"></script>
|
||||||
<script src="js/lib/layers.js?v=a1f8aea" type="text/javascript"></script>
|
<script src="js/lib/layers.js?v=1a452a1" type="text/javascript"></script>
|
||||||
<script src="js/lib/commands.js?v=bf23c83" type="text/javascript"></script>
|
<script src="js/lib/commands.js?v=ad60afc" type="text/javascript"></script>
|
||||||
|
|
||||||
<script src="js/lib/toolbar.js?v=306d637" type="text/javascript"></script>
|
<script src="js/lib/toolbar.js?v=306d637" type="text/javascript"></script>
|
||||||
<script src="js/lib/ui.js?v=fe9b702" type="text/javascript"></script>
|
<script src="js/lib/ui.js?v=17014b3" type="text/javascript"></script>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
src="js/initalize/layers.populate.js?v=066dc8e"
|
src="js/initalize/layers.populate.js?v=066dc8e"
|
||||||
|
@ -367,13 +422,13 @@
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
||||||
<script src="js/index.js?v=840dc09" type="text/javascript"></script>
|
<script src="js/index.js?v=c17a2b3" type="text/javascript"></script>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
src="js/ui/floating/history.js?v=fc92d14"
|
src="js/ui/floating/history.js?v=4f29db4"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/floating/layers.js?v=8e66543"
|
src="js/ui/floating/layers.js?v=aa78ec8"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Load Tools -->
|
<!-- Load Tools -->
|
||||||
|
@ -381,22 +436,25 @@
|
||||||
src="js/ui/tool/generic.js?v=3e678e0"
|
src="js/ui/tool/generic.js?v=3e678e0"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
||||||
<script src="js/ui/tool/dream.js?v=74c0c1f" type="text/javascript"></script>
|
<script src="js/ui/tool/dream.js?v=5be34c1" type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/maskbrush.js?v=d88810f"
|
src="js/ui/tool/maskbrush.js?v=d88810f"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/colorbrush.js?v=6f1d2f4"
|
src="js/ui/tool/colorbrush.js?v=46011ee"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/select.js?v=f290e83"
|
src="js/ui/tool/select.js?v=63fe672"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
<script src="js/ui/tool/stamp.js?v=4a86ff8" type="text/javascript"></script>
|
<script src="js/ui/tool/stamp.js?v=d89b15d" type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/interrogate.js?v=e579ff1"
|
src="js/ui/tool/interrogate.js?v=e579ff1"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Initialize -->
|
<!-- Initialize -->
|
||||||
|
<script
|
||||||
|
src="js/initalize/workspace.populate.js?v=e5586da"
|
||||||
|
type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/initalize/shortcuts.populate.js?v=fd01c47"
|
src="js/initalize/shortcuts.populate.js?v=fd01c47"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
28
js/defaults.js
Normal file
28
js/defaults.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Default settings for local configurations
|
||||||
|
*/
|
||||||
|
const localDefaults = {
|
||||||
|
/** Default Host */
|
||||||
|
host: "http://127.0.0.1:7860",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default settings for workspace configurations
|
||||||
|
*/
|
||||||
|
const workspaceDefaults = {
|
||||||
|
/** Default Prompt - REQ */
|
||||||
|
prompt: "ocean floor scientific expedition, underwater wildlife",
|
||||||
|
/** Default Negative Prompt - REQ */
|
||||||
|
neg_prompt:
|
||||||
|
"people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry",
|
||||||
|
/** Default Stable Diffusion Seed - REQ */
|
||||||
|
seed: -1,
|
||||||
|
|
||||||
|
/** Default CFG Scale - REQ */
|
||||||
|
cfg_scale: 7.0,
|
||||||
|
/** Default steps - REQ */
|
||||||
|
steps: 30,
|
||||||
|
|
||||||
|
/** Default Resolution */
|
||||||
|
resolution: 512,
|
||||||
|
};
|
142
js/index.js
142
js/index.js
|
@ -145,6 +145,19 @@ var url = "/sdapi/v1/";
|
||||||
const basePixelCount = 64; //64 px - ALWAYS 64 PX
|
const basePixelCount = 64; //64 px - ALWAYS 64 PX
|
||||||
var focused = true;
|
var focused = true;
|
||||||
|
|
||||||
|
function getSDData() {
|
||||||
|
const w = workspaces.current.settings;
|
||||||
|
w.ste;
|
||||||
|
return {
|
||||||
|
prompt: w.prompt,
|
||||||
|
negative_prompt: w.neg_prompt,
|
||||||
|
seed: w.seed,
|
||||||
|
|
||||||
|
cfg_scale: w.cfg_scale,
|
||||||
|
steps: w.steps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function startup() {
|
function startup() {
|
||||||
testHostConfiguration();
|
testHostConfiguration();
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
@ -210,8 +223,6 @@ function testHostConfiguration() {
|
||||||
host = value;
|
host = value;
|
||||||
hostEl.value = host;
|
hostEl.value = host;
|
||||||
localStorage.setItem("openoutpaint/host", host);
|
localStorage.setItem("openoutpaint/host", host);
|
||||||
|
|
||||||
testHostConfiguration();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const current = localStorage.getItem("openoutpaint/host");
|
const current = localStorage.getItem("openoutpaint/host");
|
||||||
|
@ -487,10 +498,19 @@ async function testHostConnection() {
|
||||||
function newImage(evt) {
|
function newImage(evt) {
|
||||||
clearPaintedMask();
|
clearPaintedMask();
|
||||||
uil.layers.forEach(({layer}) => {
|
uil.layers.forEach(({layer}) => {
|
||||||
commands.runCommand("eraseImage", "Clear Canvas", {
|
commands.runCommand(
|
||||||
...layer.bb,
|
"eraseImage",
|
||||||
ctx: layer.ctx,
|
"Clear Canvas",
|
||||||
});
|
{
|
||||||
|
...layer.bb,
|
||||||
|
ctx: layer.ctx,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: `Cleared Canvas`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,16 +587,16 @@ const makeSlider = (
|
||||||
textStep = null,
|
textStep = null,
|
||||||
valuecb = null
|
valuecb = null
|
||||||
) => {
|
) => {
|
||||||
const local = lsKey && localStorage.getItem(`openoutpaint/${lsKey}`);
|
const local = lsKey && workspaces.current.settings[lsKey];
|
||||||
const def = parseFloat(local === null ? defaultValue : local);
|
const def = parseFloat(local === null ? defaultValue : local);
|
||||||
let cb = (v) => {
|
let cb = (v) => {
|
||||||
stableDiffusionData[lsKey] = v;
|
stableDiffusionData[lsKey] = v;
|
||||||
if (lsKey) localStorage.setItem(`openoutpaint/${lsKey}`, v);
|
if (lsKey) workspaces.current.settings[lsKey] = v;
|
||||||
};
|
};
|
||||||
if (valuecb) {
|
if (valuecb) {
|
||||||
cb = (v) => {
|
cb = (v) => {
|
||||||
valuecb(v);
|
valuecb(v);
|
||||||
localStorage.setItem(`openoutpaint/${lsKey}`, v);
|
if (lsKey) workspaces.current.settings[lsKey] = v;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return createSlider(label, el, {
|
return createSlider(label, el, {
|
||||||
|
@ -848,6 +868,103 @@ function drawBackground() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportWorkspaceState() {
|
||||||
|
return {
|
||||||
|
defaultLayer: {
|
||||||
|
id: uil.layerIndex.default.id,
|
||||||
|
name: uil.layerIndex.default.name,
|
||||||
|
},
|
||||||
|
bb: {
|
||||||
|
x: imageCollection.bb.x,
|
||||||
|
y: imageCollection.bb.y,
|
||||||
|
w: imageCollection.bb.w,
|
||||||
|
h: imageCollection.bb.h,
|
||||||
|
},
|
||||||
|
history: await commands.export(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importWorkspaceState(state) {
|
||||||
|
// Start from zero, effectively
|
||||||
|
await commands.clear();
|
||||||
|
|
||||||
|
// Setup initial layer
|
||||||
|
const layer = uil.layerIndex.default;
|
||||||
|
layer.deletable = true;
|
||||||
|
|
||||||
|
await commands.runCommand(
|
||||||
|
"addLayer",
|
||||||
|
"Temporary Layer",
|
||||||
|
{name: "Temporary Layer", key: "tmp"},
|
||||||
|
{recordHistory: false}
|
||||||
|
);
|
||||||
|
|
||||||
|
await commands.runCommand(
|
||||||
|
"deleteLayer",
|
||||||
|
"Deleted Layer",
|
||||||
|
{
|
||||||
|
layer,
|
||||||
|
},
|
||||||
|
{recordHistory: false}
|
||||||
|
);
|
||||||
|
|
||||||
|
await commands.runCommand(
|
||||||
|
"addLayer",
|
||||||
|
"Initial Layer Creation",
|
||||||
|
{
|
||||||
|
id: state.defaultLayer.id,
|
||||||
|
name: state.defaultLayer.name,
|
||||||
|
key: "default",
|
||||||
|
deletable: false,
|
||||||
|
},
|
||||||
|
{recordHistory: false}
|
||||||
|
);
|
||||||
|
|
||||||
|
await commands.runCommand(
|
||||||
|
"deleteLayer",
|
||||||
|
"Deleted Layer",
|
||||||
|
{
|
||||||
|
layer: uil.layerIndex.tmp,
|
||||||
|
},
|
||||||
|
{recordHistory: false}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resize canvas to match original size
|
||||||
|
const sbb = new BoundingBox(state.bb);
|
||||||
|
|
||||||
|
const bb = imageCollection.bb;
|
||||||
|
let eleft = 0;
|
||||||
|
if (bb.x > sbb.x) eleft = bb.x - sbb.x;
|
||||||
|
let etop = 0;
|
||||||
|
if (bb.y > sbb.y) etop = bb.y - sbb.y;
|
||||||
|
|
||||||
|
let eright = 0;
|
||||||
|
if (bb.tr.x < sbb.tr.x) eright = sbb.tr.x - bb.tr.x;
|
||||||
|
let ebottom = 0;
|
||||||
|
if (bb.br.y < sbb.br.y) ebottom = sbb.br.y - bb.br.y;
|
||||||
|
|
||||||
|
imageCollection.expand(eleft, etop, eright, ebottom);
|
||||||
|
|
||||||
|
// Run commands in order
|
||||||
|
for (const command of state.history) {
|
||||||
|
await commands.import(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWorkspaceToFile() {
|
||||||
|
const workspace = await exportWorkspaceState();
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(workspace)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
var link = document.createElement("a"); // Or maybe get it from the current document
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${new Date().toISOString()}_openOutpaint_workspace.json`;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
async function getUpscalers() {
|
async function getUpscalers() {
|
||||||
var url = document.getElementById("host").value + "/sdapi/v1/upscalers";
|
var url = document.getElementById("host").value + "/sdapi/v1/upscalers";
|
||||||
let upscalers = [];
|
let upscalers = [];
|
||||||
|
@ -1167,11 +1284,6 @@ function loadSettings() {
|
||||||
localStorage.getItem("openoutpaint/mask_blur") == null
|
localStorage.getItem("openoutpaint/mask_blur") == null
|
||||||
? 0
|
? 0
|
||||||
: localStorage.getItem("openoutpaint/mask_blur");
|
: localStorage.getItem("openoutpaint/mask_blur");
|
||||||
var _seed =
|
|
||||||
localStorage.getItem("openoutpaint/seed") == null
|
|
||||||
? -1
|
|
||||||
: localStorage.getItem("openoutpaint/seed");
|
|
||||||
|
|
||||||
let _enable_hr =
|
let _enable_hr =
|
||||||
localStorage.getItem("openoutpaint/enable_hr") === null
|
localStorage.getItem("openoutpaint/enable_hr") === null
|
||||||
? false
|
? false
|
||||||
|
@ -1202,7 +1314,7 @@ function loadSettings() {
|
||||||
|
|
||||||
// set the values into the UI
|
// set the values into the UI
|
||||||
document.getElementById("maskBlur").value = Number(_mask_blur);
|
document.getElementById("maskBlur").value = Number(_mask_blur);
|
||||||
document.getElementById("seed").value = Number(_seed);
|
document.getElementById("seed").value = workspaces.current.settings.seed;
|
||||||
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
||||||
document.getElementById("cbxRestoreFaces").checked = Boolean(_restore_faces);
|
document.getElementById("cbxRestoreFaces").checked = Boolean(_restore_faces);
|
||||||
document.getElementById("cbxSyncCursorSize").checked =
|
document.getElementById("cbxSyncCursorSize").checked =
|
||||||
|
|
178
js/initalize/workspace.populate.js
Normal file
178
js/initalize/workspace.populate.js
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
(() => {
|
||||||
|
const saveWorkspaceBtn = document.getElementById("save-workspace-btn");
|
||||||
|
const renameWorkspaceBtn = document.getElementById("rename-workspace-btn");
|
||||||
|
const moreWorkspaceBtn = document.getElementById("more-workspace-btn");
|
||||||
|
const expandedWorkspaceMenu = document.getElementById("more-workspace-menu");
|
||||||
|
const exportWorkspaceBtn = document.getElementById("export-workspace-btn");
|
||||||
|
const importWorkspaceBtn = document.getElementById("import-workspace-btn");
|
||||||
|
const deleteWorkspaceBtn = document.getElementById("delete-workspace-btn");
|
||||||
|
|
||||||
|
moreWorkspaceBtn.addEventListener("click", () => {
|
||||||
|
expandedWorkspaceMenu.classList.toggle("collapsed");
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceAutocomplete = createAutoComplete(
|
||||||
|
"Workspace",
|
||||||
|
document.getElementById("workspace-select")
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceAutocomplete.options = [{name: "Default", value: "default"}];
|
||||||
|
workspaceAutocomplete.value = "default";
|
||||||
|
renameWorkspaceBtn.disabled = true;
|
||||||
|
deleteWorkspaceBtn.disabled = true;
|
||||||
|
|
||||||
|
workspaceAutocomplete.onchange.on(async ({name, value}) => {
|
||||||
|
if (value === "default") {
|
||||||
|
renameWorkspaceBtn.disabled = true;
|
||||||
|
deleteWorkspaceBtn.disabled = true;
|
||||||
|
await commands.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renameWorkspaceBtn.disabled = false;
|
||||||
|
deleteWorkspaceBtn.disabled = false;
|
||||||
|
|
||||||
|
const workspaces = db
|
||||||
|
.transaction("workspaces", "readonly")
|
||||||
|
.objectStore("workspaces");
|
||||||
|
|
||||||
|
workspaces.get(value).onsuccess = (e) => {
|
||||||
|
console.debug("[workspace.populate] Loading workspace");
|
||||||
|
|
||||||
|
const res = e.target.result;
|
||||||
|
const {workspace} = res;
|
||||||
|
importWorkspaceState(workspace);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Workspace selection list
|
||||||
|
*/
|
||||||
|
const listWorkspaces = async (value = undefined) => {
|
||||||
|
const options = [{name: "Default", value: "default"}];
|
||||||
|
|
||||||
|
const workspaces = db
|
||||||
|
.transaction("workspaces", "readonly")
|
||||||
|
.objectStore("workspaces");
|
||||||
|
|
||||||
|
workspaces.openCursor().onsuccess = (e) => {
|
||||||
|
/** @type {IDBCursor} */
|
||||||
|
const c = e.target.result;
|
||||||
|
if (c) {
|
||||||
|
options.push({name: c.value.name, value: c.key});
|
||||||
|
c.continue();
|
||||||
|
} else {
|
||||||
|
const previousValue = workspaceAutocomplete.value;
|
||||||
|
|
||||||
|
workspaceAutocomplete.options = options;
|
||||||
|
workspaceAutocomplete.value = value ?? previousValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveWorkspaceToDB = async (value) => {
|
||||||
|
const workspace = await exportWorkspaceState();
|
||||||
|
|
||||||
|
const workspaces = db
|
||||||
|
.transaction("workspaces", "readwrite")
|
||||||
|
.objectStore("workspaces");
|
||||||
|
|
||||||
|
let id = value;
|
||||||
|
if (value === "default" && commands._history.length > 0) {
|
||||||
|
// If Workspace is the Default
|
||||||
|
const name = (prompt("Please enter the workspace name") ?? "").trim();
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
id = guid();
|
||||||
|
workspaces.add({id, name, workspace}).onsuccess = () => {
|
||||||
|
listWorkspaces(id);
|
||||||
|
alert(`Workspace saved as '${name}'`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
workspaces.get(id).onsuccess = (e) => {
|
||||||
|
const ws = e.target.result;
|
||||||
|
if (ws) {
|
||||||
|
workspaces.put({id, workspace}).onsuccess = () => {
|
||||||
|
alert(`Workspace saved as '${ws.value.name}'`);
|
||||||
|
listWorkspaces();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normal Workspace Export/Import
|
||||||
|
exportWorkspaceBtn.addEventListener("click", () => saveWorkspaceToFile());
|
||||||
|
importWorkspaceBtn.addEventListener("click", () => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = "application/json";
|
||||||
|
input.addEventListener("change", async (evn) => {
|
||||||
|
let files = Array.from(input.files);
|
||||||
|
const json = await files[0].text();
|
||||||
|
|
||||||
|
await importWorkspaceState(JSON.parse(json));
|
||||||
|
saveWorkspaceToDB("default");
|
||||||
|
});
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDatabaseLoad = async () => {
|
||||||
|
// Get workspaces from database
|
||||||
|
listWorkspaces();
|
||||||
|
|
||||||
|
// Save Workspace Button
|
||||||
|
saveWorkspaceBtn.addEventListener("click", () =>
|
||||||
|
saveWorkspaceToDB(workspaceAutocomplete.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rename Workspace
|
||||||
|
renameWorkspaceBtn.addEventListener("click", () => {
|
||||||
|
const workspaces = db
|
||||||
|
.transaction("workspaces", "readwrite")
|
||||||
|
.objectStore("workspaces");
|
||||||
|
|
||||||
|
let id = workspaceAutocomplete.value;
|
||||||
|
|
||||||
|
workspaces.get(id).onsuccess = (e) => {
|
||||||
|
const workspace = e.target.result;
|
||||||
|
const name = prompt(
|
||||||
|
`Please enter the new workspace name.\nOriginal is '${workspace.name}'`
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
workspace.name = name;
|
||||||
|
|
||||||
|
workspaces.put(workspace).onsuccess = () => {
|
||||||
|
listWorkspaces();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Delete Workspace
|
||||||
|
deleteWorkspaceBtn.addEventListener("click", () => {
|
||||||
|
const workspaces = db
|
||||||
|
.transaction("workspaces", "readwrite")
|
||||||
|
.objectStore("workspaces");
|
||||||
|
|
||||||
|
let id = workspaceAutocomplete.value;
|
||||||
|
|
||||||
|
workspaces.get(id).onsuccess = (e) => {
|
||||||
|
const workspace = e.target.result;
|
||||||
|
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
`Do you really want to delete the workspace '${workspace.name}'?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
workspaces.delete(id).onsuccess = (e) => {
|
||||||
|
listWorkspaces("default");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (db) onDatabaseLoad();
|
||||||
|
else ondatabaseload.on(onDatabaseLoad);
|
||||||
|
})();
|
|
@ -6,7 +6,9 @@
|
||||||
* @property {string} title The title passed to the command being run
|
* @property {string} title The title passed to the command being run
|
||||||
* @property {() => void | Promise<void>} undo A method to undo whatever the command did
|
* @property {() => void | Promise<void>} undo A method to undo whatever the command did
|
||||||
* @property {() => void | Promise<void>} redo A method to redo whatever undo did
|
* @property {() => void | Promise<void>} redo A method to redo whatever undo did
|
||||||
|
* @property {() => any | Promise<any>} export A method to export the command
|
||||||
* @property {{[key: string]: any}} state The state of the current command instance
|
* @property {{[key: string]: any}} state The state of the current command instance
|
||||||
|
* @property {{[key: string]: any}} extra Extra information saved with the command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,6 +16,7 @@
|
||||||
*
|
*
|
||||||
* @typedef CommandExtraParams
|
* @typedef CommandExtraParams
|
||||||
* @property {boolean} recordHistory The title passed to the command being run
|
* @property {boolean} recordHistory The title passed to the command being run
|
||||||
|
* @property {any} importData Data to restore the command from
|
||||||
* @property {Record<string, any>} extra Extra information to be stored in the history entry
|
* @property {Record<string, any>} extra Extra information to be stored in the history entry
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,44 @@ const commands = makeReadOnly(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the history
|
||||||
|
*/
|
||||||
|
async clear() {
|
||||||
|
await this.undo(this._history.length);
|
||||||
|
|
||||||
|
this._history.splice(0, this._history.length);
|
||||||
|
|
||||||
|
_commands_events.emit({
|
||||||
|
action: "clear",
|
||||||
|
state: {},
|
||||||
|
current: commands._current,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports an exported command and runs it
|
||||||
|
*
|
||||||
|
* @param {{name: string, title: string, data: any}} exported Exported command
|
||||||
|
*/
|
||||||
|
async import(exported) {
|
||||||
|
await this.runCommand(
|
||||||
|
exported.command,
|
||||||
|
exported.title,
|
||||||
|
{},
|
||||||
|
{importData: exported.data}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all commands in the history
|
||||||
|
*/
|
||||||
|
async export() {
|
||||||
|
return Promise.all(
|
||||||
|
this._history.map(async (command) => command.export())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a basic command, that can be done and undone
|
* Creates a basic command, that can be done and undone
|
||||||
*
|
*
|
||||||
|
@ -74,31 +112,57 @@ const commands = makeReadOnly(
|
||||||
* @param {string} name Command identifier (name)
|
* @param {string} name Command identifier (name)
|
||||||
* @param {CommandDoCallback} run A method that performs the action for the first time
|
* @param {CommandDoCallback} run A method that performs the action for the first time
|
||||||
* @param {CommandUndoCallback} undo A method that reverses what the run method did
|
* @param {CommandUndoCallback} undo A method that reverses what the run method did
|
||||||
* @param {object} options Extra options
|
* @param {object} opt Extra options
|
||||||
* @param {CommandDoCallback} options.redo A method that redoes the action after undone (default: run)
|
* @param {CommandDoCallback} opt.redo A method that redoes the action after undone (default: run)
|
||||||
|
* @param {(state: any) => any} opt.exportfn A method that exports a serializeable object
|
||||||
|
* @param {(value: any, state: any) => any} opt.importfn A method that imports a serializeable object
|
||||||
* @returns {Command}
|
* @returns {Command}
|
||||||
*/
|
*/
|
||||||
createCommand(name, run, undo, options = {}) {
|
createCommand(name, run, undo, opt = {}) {
|
||||||
defaultOpt(options, {
|
defaultOpt(opt, {
|
||||||
redo: run,
|
redo: run,
|
||||||
|
exportfn: null,
|
||||||
|
importfn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const redo = options.redo;
|
const command = async function runWrapper(title, options, extra = {}) {
|
||||||
|
|
||||||
const command = async function runWrapper(title, options, extra) {
|
|
||||||
// Create copy of options and state object
|
// Create copy of options and state object
|
||||||
const copy = {};
|
const copy = {};
|
||||||
Object.assign(copy, options);
|
Object.assign(copy, options);
|
||||||
const state = {};
|
const state = {};
|
||||||
|
|
||||||
|
defaultOpt(extra, {
|
||||||
|
recordHistory: true,
|
||||||
|
importData: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportfn =
|
||||||
|
opt.exportfn ?? ((state) => Object.assign({}, state.serializeable));
|
||||||
|
const importfn =
|
||||||
|
opt.importfn ??
|
||||||
|
((value, state) => (state.serializeable = Object.assign({}, value)));
|
||||||
|
const redo = opt.redo;
|
||||||
|
|
||||||
/** @type {CommandEntry} */
|
/** @type {CommandEntry} */
|
||||||
const entry = {
|
const entry = {
|
||||||
id: guid(),
|
id: guid(),
|
||||||
title,
|
title,
|
||||||
state,
|
state,
|
||||||
|
async export() {
|
||||||
|
return {
|
||||||
|
command: name,
|
||||||
|
title,
|
||||||
|
data: await exportfn(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
extra: extra.extra,
|
extra: extra.extra,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (extra.importData) {
|
||||||
|
await importfn(extra.importData, state);
|
||||||
|
state.imported = extra.importData;
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to run command
|
// Attempt to run command
|
||||||
try {
|
try {
|
||||||
console.debug(`[commands] Running '${title}'[${name}]`);
|
console.debug(`[commands] Running '${title}'[${name}]`);
|
||||||
|
@ -209,47 +273,63 @@ commands.createCommand(
|
||||||
"drawImage",
|
"drawImage",
|
||||||
(title, options, state) => {
|
(title, options, state) => {
|
||||||
if (
|
if (
|
||||||
!options ||
|
!state.imported &&
|
||||||
options.image === undefined ||
|
(!options ||
|
||||||
options.x === undefined ||
|
options.image === undefined ||
|
||||||
options.y === undefined
|
options.x === undefined ||
|
||||||
|
options.y === undefined)
|
||||||
)
|
)
|
||||||
throw "Command drawImage requires options in the format: {image, x, y, w?, h?, ctx?}";
|
throw "Command drawImage requires options in the format: {image, x, y, w?, h?, layer?}";
|
||||||
|
|
||||||
// Check if we have state
|
// Check if we have state
|
||||||
if (!state.context) {
|
if (!state.layer) {
|
||||||
const context = options.ctx || uil.ctx;
|
/** @type {Layer} */
|
||||||
state.context = context;
|
let layer = options.layer;
|
||||||
|
if (!options.layer && state.layerId)
|
||||||
|
layer = imageCollection.layers[state.layerId];
|
||||||
|
|
||||||
// Saving what was in the canvas before the command
|
if (!options.layer && !state.layerId) layer = uil.layer;
|
||||||
const imgData = context.getImageData(
|
|
||||||
options.x,
|
state.layer = layer;
|
||||||
options.y,
|
state.context = layer.ctx;
|
||||||
options.w || options.image.width,
|
|
||||||
options.h || options.image.height
|
if (!state.imported) {
|
||||||
);
|
const canvas = document.createElement("canvas");
|
||||||
state.box = {
|
canvas.width = options.image.width;
|
||||||
x: options.x,
|
canvas.height = options.image.height;
|
||||||
y: options.y,
|
canvas.getContext("2d").drawImage(options.image, 0, 0);
|
||||||
w: options.w || options.image.width,
|
|
||||||
h: options.h || options.image.height,
|
state.image = canvas;
|
||||||
};
|
|
||||||
// Create Image
|
// Saving what was in the canvas before the command
|
||||||
const cutout = document.createElement("canvas");
|
const imgData = state.context.getImageData(
|
||||||
cutout.width = state.box.w;
|
options.x,
|
||||||
cutout.height = state.box.h;
|
options.y,
|
||||||
cutout.getContext("2d").putImageData(imgData, 0, 0);
|
options.w || options.image.width,
|
||||||
state.original = new Image();
|
options.h || options.image.height
|
||||||
state.original.src = cutout.toDataURL();
|
);
|
||||||
|
state.box = {
|
||||||
|
x: options.x,
|
||||||
|
y: options.y,
|
||||||
|
w: options.w || options.image.width,
|
||||||
|
h: options.h || options.image.height,
|
||||||
|
};
|
||||||
|
// 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 = cutout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply command
|
// Apply command
|
||||||
state.context.drawImage(
|
state.context.drawImage(
|
||||||
options.image,
|
state.image,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
options.image.width,
|
state.image.width,
|
||||||
options.image.height,
|
state.image.height,
|
||||||
state.box.x,
|
state.box.x,
|
||||||
state.box.y,
|
state.box.y,
|
||||||
state.box.w,
|
state.box.w,
|
||||||
|
@ -261,6 +341,51 @@ commands.createCommand(
|
||||||
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
||||||
// Undo
|
// Undo
|
||||||
state.context.drawImage(state.original, state.box.x, state.box.y);
|
state.context.drawImage(state.original, state.box.x, state.box.y);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exportfn: (state) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = state.image.width;
|
||||||
|
canvas.height = state.image.height;
|
||||||
|
canvas.getContext("2d").drawImage(state.image, 0, 0);
|
||||||
|
|
||||||
|
const originalc = document.createElement("canvas");
|
||||||
|
originalc.width = state.original.width;
|
||||||
|
originalc.height = state.original.height;
|
||||||
|
originalc.getContext("2d").drawImage(state.original, 0, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
image: canvas.toDataURL(),
|
||||||
|
original: originalc.toDataURL(),
|
||||||
|
box: state.box,
|
||||||
|
layer: state.layer.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
importfn: async (value, state) => {
|
||||||
|
state.box = value.box;
|
||||||
|
state.layerId = value.layer;
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = value.image;
|
||||||
|
await img.decode();
|
||||||
|
|
||||||
|
const imagec = document.createElement("canvas");
|
||||||
|
imagec.width = state.box.w;
|
||||||
|
imagec.height = state.box.h;
|
||||||
|
imagec.getContext("2d").drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
const orig = document.createElement("img");
|
||||||
|
orig.src = value.original;
|
||||||
|
await orig.decode();
|
||||||
|
|
||||||
|
const originalc = document.createElement("canvas");
|
||||||
|
originalc.width = state.box.w;
|
||||||
|
originalc.height = state.box.h;
|
||||||
|
originalc.getContext("2d").drawImage(orig, 0, 0);
|
||||||
|
|
||||||
|
state.image = imagec;
|
||||||
|
state.original = originalc;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -268,18 +393,26 @@ commands.createCommand(
|
||||||
"eraseImage",
|
"eraseImage",
|
||||||
(title, options, state) => {
|
(title, options, state) => {
|
||||||
if (
|
if (
|
||||||
!options ||
|
!state.imported &&
|
||||||
options.x === undefined ||
|
(!options ||
|
||||||
options.y === undefined ||
|
options.x === undefined ||
|
||||||
options.w === undefined ||
|
options.y === undefined ||
|
||||||
options.h === undefined
|
options.w === undefined ||
|
||||||
|
options.h === undefined)
|
||||||
)
|
)
|
||||||
throw "Command eraseImage requires options in the format: {x, y, w, h, ctx?}";
|
throw "Command eraseImage requires options in the format: {x, y, w, h, ctx?}";
|
||||||
|
|
||||||
|
if (state.imported) {
|
||||||
|
state.layer = imageCollection.layers[state.layerId];
|
||||||
|
state.context = state.layer.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have state
|
// Check if we have state
|
||||||
if (!state.context) {
|
if (!state.layer) {
|
||||||
const context = options.ctx || uil.ctx;
|
const layer = (options.layer || state.layerId) ?? uil.layer;
|
||||||
state.context = context;
|
state.layer = layer;
|
||||||
|
state.mask = options.mask;
|
||||||
|
state.context = layer.ctx;
|
||||||
|
|
||||||
// Saving what was in the canvas before the command
|
// Saving what was in the canvas before the command
|
||||||
state.box = {
|
state.box = {
|
||||||
|
@ -295,7 +428,7 @@ commands.createCommand(
|
||||||
cutout
|
cutout
|
||||||
.getContext("2d")
|
.getContext("2d")
|
||||||
.drawImage(
|
.drawImage(
|
||||||
context.canvas,
|
state.context.canvas,
|
||||||
options.x,
|
options.x,
|
||||||
options.y,
|
options.y,
|
||||||
options.w,
|
options.w,
|
||||||
|
@ -316,9 +449,9 @@ commands.createCommand(
|
||||||
const op = state.context.globalCompositeOperation;
|
const op = state.context.globalCompositeOperation;
|
||||||
state.context.globalCompositeOperation = "destination-out";
|
state.context.globalCompositeOperation = "destination-out";
|
||||||
|
|
||||||
if (options.mask)
|
if (state.mask)
|
||||||
state.context.drawImage(
|
state.context.drawImage(
|
||||||
options.mask,
|
state.mask,
|
||||||
state.box.x,
|
state.box.x,
|
||||||
state.box.y,
|
state.box.y,
|
||||||
state.box.w,
|
state.box.w,
|
||||||
|
@ -340,5 +473,59 @@ commands.createCommand(
|
||||||
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
||||||
// Undo
|
// Undo
|
||||||
state.context.drawImage(state.original, state.box.x, state.box.y);
|
state.context.drawImage(state.original, state.box.x, state.box.y);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exportfn: (state) => {
|
||||||
|
let mask = null;
|
||||||
|
|
||||||
|
if (state.mask) {
|
||||||
|
const maskc = document.createElement("canvas");
|
||||||
|
maskc.width = state.mask.width;
|
||||||
|
maskc.height = state.mask.height;
|
||||||
|
maskc.getContext("2d").drawImage(state.mask, 0, 0);
|
||||||
|
|
||||||
|
mask = maskc.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalc = document.createElement("canvas");
|
||||||
|
originalc.width = state.original.width;
|
||||||
|
originalc.height = state.original.height;
|
||||||
|
originalc.getContext("2d").drawImage(state.original, 0, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
original: originalc.toDataURL(),
|
||||||
|
mask,
|
||||||
|
box: state.box,
|
||||||
|
layer: state.layer.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
importfn: async (value, state) => {
|
||||||
|
state.box = value.box;
|
||||||
|
state.layerId = value.layer;
|
||||||
|
|
||||||
|
if (value.mask) {
|
||||||
|
const mask = document.createElement("img");
|
||||||
|
mask.src = value.mask;
|
||||||
|
await mask.decode();
|
||||||
|
|
||||||
|
const maskc = document.createElement("canvas");
|
||||||
|
maskc.width = state.box.w;
|
||||||
|
maskc.height = state.box.h;
|
||||||
|
maskc.getContext("2d").drawImage(mask, 0, 0);
|
||||||
|
|
||||||
|
state.mask = maskc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orig = document.createElement("img");
|
||||||
|
orig.src = value.original;
|
||||||
|
await orig.decode();
|
||||||
|
|
||||||
|
const originalc = document.createElement("canvas");
|
||||||
|
originalc.width = state.box.w;
|
||||||
|
originalc.height = state.box.h;
|
||||||
|
originalc.getContext("2d").drawImage(orig, 0, 0);
|
||||||
|
|
||||||
|
state.original = originalc;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
36
js/lib/db.js
Normal file
36
js/lib/db.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const idb = window.indexedDB.open("openoutpaint", 2);
|
||||||
|
|
||||||
|
idb.onerror = (e) => {
|
||||||
|
console.warn("[stamp] Failed to connect to IndexedDB");
|
||||||
|
console.warn(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
idb.onupgradeneeded = (e) => {
|
||||||
|
const db = e.target.result;
|
||||||
|
|
||||||
|
console.debug(`[stamp] Setting up database version ${db.version}`);
|
||||||
|
|
||||||
|
if (e.oldVersion < 1) {
|
||||||
|
// Resources Store
|
||||||
|
const resourcesStore = db.createObjectStore("resources", {
|
||||||
|
keyPath: "id",
|
||||||
|
});
|
||||||
|
resourcesStore.createIndex("name", "name", {unique: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspaces Store
|
||||||
|
const workspacesStore = db.createObjectStore("workspaces", {
|
||||||
|
keyPath: "id",
|
||||||
|
});
|
||||||
|
workspacesStore.createIndex("name", "name", {unique: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {IDBDatabase} */
|
||||||
|
let db = null;
|
||||||
|
/** @type {Observer<{db: IDBDatabase}>} */
|
||||||
|
const ondatabaseload = new Observer();
|
||||||
|
|
||||||
|
idb.onsuccess = (e) => {
|
||||||
|
db = e.target.result;
|
||||||
|
ondatabaseload.emit({db});
|
||||||
|
};
|
|
@ -38,7 +38,7 @@
|
||||||
* @property {Point} inputOffset The offset for calculating layer coordinates from input element input information
|
* @property {Point} inputOffset The offset for calculating layer coordinates from input element input information
|
||||||
* @property {Point} origin The location of the origin ((0, 0) point) of the collection (If canvas goes from -64, -32 to 128, 512, it's (64, 32))
|
* @property {Point} origin The location of the origin ((0, 0) point) of the collection (If canvas goes from -64, -32 to 128, 512, it's (64, 32))
|
||||||
* @property {BoundingBox} bb The current bounding box of the collection, in layer coordinates
|
* @property {BoundingBox} bb The current bounding box of the collection, in layer coordinates
|
||||||
* @property {{[key: string]: Layer}} layers An object for quick access to named layers of the collection
|
* @property {{[key: string]: Layer}} layers An object for quick access to layers of the collection
|
||||||
* @property {Size} size The size of the collection (CSS)
|
* @property {Size} size The size of the collection (CSS)
|
||||||
* @property {Size} resolution The resolution of the collection (canvas)
|
* @property {Size} resolution The resolution of the collection (canvas)
|
||||||
* @property {function} expand Expands the collection and its full layers by the specified amounts
|
* @property {function} expand Expands the collection and its full layers by the specified amounts
|
||||||
|
|
|
@ -335,6 +335,7 @@ const layers = {
|
||||||
*
|
*
|
||||||
* @param {string | null} key Name and key to use to access layer. If null, it is a temporary layer.
|
* @param {string | null} key Name and key to use to access layer. If null, it is a temporary layer.
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
|
* @param {string} options.id
|
||||||
* @param {string} options.name
|
* @param {string} options.name
|
||||||
* @param {?BoundingBox} options.bb
|
* @param {?BoundingBox} options.bb
|
||||||
* @param {string} [options.category]
|
* @param {string} [options.category]
|
||||||
|
@ -346,9 +347,12 @@ const layers = {
|
||||||
*/
|
*/
|
||||||
registerLayer(key = null, options = {}) {
|
registerLayer(key = null, options = {}) {
|
||||||
// Make ID
|
// Make ID
|
||||||
const id = guid();
|
const id = options.id ?? guid();
|
||||||
|
|
||||||
defaultOpt(options, {
|
defaultOpt(options, {
|
||||||
|
// ID of the layer
|
||||||
|
id: null,
|
||||||
|
|
||||||
// Display name for the layer
|
// Display name for the layer
|
||||||
name: key || `Temporary ${id}`,
|
name: key || `Temporary ${id}`,
|
||||||
|
|
||||||
|
@ -618,6 +622,7 @@ const layers = {
|
||||||
collection._layers.splice(index, 0, layer);
|
collection._layers.splice(index, 0, layer);
|
||||||
}
|
}
|
||||||
if (key) collection.layers[key] = layer;
|
if (key) collection.layers[key] = layer;
|
||||||
|
collection.layers[id] = layer;
|
||||||
|
|
||||||
if (key === null)
|
if (key === null)
|
||||||
console.debug(
|
console.debug(
|
||||||
|
@ -651,7 +656,8 @@ const layers = {
|
||||||
layers.listen.onlayerdelete.emit({
|
layers.listen.onlayerdelete.emit({
|
||||||
layer: lobj,
|
layer: lobj,
|
||||||
});
|
});
|
||||||
if (lobj.key) delete collection.layers[lobj.key];
|
if (lobj.key) collection.layers[lobj.key] = undefined;
|
||||||
|
collection.layers[lobj.id] = undefined;
|
||||||
|
|
||||||
collection.element.removeChild(lobj.canvas);
|
collection.element.removeChild(lobj.canvas);
|
||||||
|
|
||||||
|
|
|
@ -209,7 +209,6 @@ function createSlider(name, wrapper, options = {}) {
|
||||||
* @param {{name: string, value: string, optionelcb: (el: HTMLOptionElement) => void}[]} options.options Options to add to the selector
|
* @param {{name: string, value: string, optionelcb: (el: HTMLOptionElement) => void}[]} options.options Options to add to the selector
|
||||||
* @param {object} extraEl Additional element to include in wrapper div (e.g. model refresh button)
|
* @param {object} extraEl Additional element to include in wrapper div (e.g. model refresh button)
|
||||||
* @param {string} extraClass Additional class to attach to the autocomplete input element
|
* @param {string} extraClass Additional class to attach to the autocomplete input element
|
||||||
* @returns {AutoCompleteElement}
|
|
||||||
*/
|
*/
|
||||||
function createAutoComplete(
|
function createAutoComplete(
|
||||||
name,
|
name,
|
||||||
|
|
0
js/lib/workspaces.d.js
Normal file
0
js/lib/workspaces.d.js
Normal file
128
js/lib/workspaces.js
Normal file
128
js/lib/workspaces.js
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
/**
|
||||||
|
* Workspaces (or sessions) are settings and canvas state storage structures that can be changed at will, saved, and restored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a workspace
|
||||||
|
*
|
||||||
|
* @template [S] Settings type
|
||||||
|
*/
|
||||||
|
class Workspace {
|
||||||
|
/**
|
||||||
|
* The name of the workspace
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
name = "Workspace Name";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workspace default settings.
|
||||||
|
*
|
||||||
|
* @type {S}
|
||||||
|
*/
|
||||||
|
defaults = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage for workspace settings.
|
||||||
|
*
|
||||||
|
* @type {S}
|
||||||
|
*/
|
||||||
|
settings = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(t, name) {
|
||||||
|
if (t[name] === undefined)
|
||||||
|
t[name] =
|
||||||
|
JSON.parse(localStorage.getItem(`openoutpaint/${name}`)) ??
|
||||||
|
defaults[name];
|
||||||
|
return t[name];
|
||||||
|
},
|
||||||
|
set(t, name, value) {
|
||||||
|
localStorage.setItem(`openoutpaint/${name}`, JSON.stringify(value));
|
||||||
|
t[name] = value;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage for other data
|
||||||
|
*
|
||||||
|
* @type {Record<string, any>}
|
||||||
|
*/
|
||||||
|
data = new Proxy({}, {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the data to the workspace
|
||||||
|
*
|
||||||
|
* @param {string} key The key of the data to be saved (eg. history or layers)
|
||||||
|
* @param {any} data The data to be saved on this key. MUST BE SERIALIZABLE.
|
||||||
|
*/
|
||||||
|
save(key, data) {
|
||||||
|
this.data[key] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets saved data from the workspace
|
||||||
|
*
|
||||||
|
* @param {string} key The key of the data to be saved (eg. history or layers)
|
||||||
|
* @param {any} data The data to be saved on this key. MUST BE SERIALIZABLE.
|
||||||
|
*/
|
||||||
|
load(key) {
|
||||||
|
return this.data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name The name of the workspace
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {S} options.defaults Default workspace settings
|
||||||
|
*/
|
||||||
|
constructor(name, options = {}) {
|
||||||
|
defaultOpt(options, {
|
||||||
|
defaults: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.defaults = options.defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaces = {
|
||||||
|
/**
|
||||||
|
* Loaded workspace
|
||||||
|
*
|
||||||
|
* @type {Workspace<workspaceDefaults>}
|
||||||
|
*/
|
||||||
|
_workspace: null,
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return this._workspace;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Workspace Changed
|
||||||
|
*
|
||||||
|
* @type {Observer<{workspace: Workspace<workspaceDefaults>}>}
|
||||||
|
*/
|
||||||
|
onchange: new Observer(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a workspace
|
||||||
|
*
|
||||||
|
* @param {Workspace<workspaceDefaults>} workspace Workspace to load
|
||||||
|
*/
|
||||||
|
loadWorkspace(workspace) {
|
||||||
|
console.info(`[workspaces] Loading workspace: ${workspace.name}`);
|
||||||
|
|
||||||
|
// Set current workspace
|
||||||
|
this._workspace = workspace;
|
||||||
|
|
||||||
|
// Notify observers that the workspace has changed
|
||||||
|
this.onchange.emit({workspace});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a new workspace instance
|
||||||
|
workspaces.loadWorkspace(
|
||||||
|
new Workspace("Default", {
|
||||||
|
workspaceDefaults,
|
||||||
|
})
|
||||||
|
);
|
|
@ -1,4 +1,20 @@
|
||||||
(() => {
|
(() => {
|
||||||
|
const historyLogBtn = document.getElementById("history-logs-btn");
|
||||||
|
historyLogBtn.addEventListener("click", () => {
|
||||||
|
let logs = "";
|
||||||
|
commands._history.forEach((entry) => {
|
||||||
|
if (entry.extra.log) logs += ` => ${entry.extra.log}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([logs], {type: "text/plain"});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
var link = document.createElement("a"); // Or maybe get it from the current document
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${new Date().toISOString()}_openOutpaint_log.txt`;
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
|
||||||
const historyView = document.getElementById("history");
|
const historyView = document.getElementById("history");
|
||||||
|
|
||||||
const makeHistoryEntry = (index, id, title) => {
|
const makeHistoryEntry = (index, id, title) => {
|
||||||
|
@ -25,7 +41,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
_commands_events.on((message) => {
|
_commands_events.on((message) => {
|
||||||
if (message.action === "run") {
|
if (message.action === "run" || message.action === "clear") {
|
||||||
Array.from(historyView.children).forEach((child) => {
|
Array.from(historyView.children).forEach((child) => {
|
||||||
if (
|
if (
|
||||||
!commands._history.find((entry) => `hist-${entry.id}` === child.id)
|
!commands._history.find((entry) => `hist-${entry.id}` === child.id)
|
||||||
|
|
|
@ -8,6 +8,7 @@ const uil = {
|
||||||
|
|
||||||
_ui_layer_list: document.getElementById("layer-list"),
|
_ui_layer_list: document.getElementById("layer-list"),
|
||||||
layers: [],
|
layers: [],
|
||||||
|
layerIndex: {},
|
||||||
_active: null,
|
_active: null,
|
||||||
set active(v) {
|
set active(v) {
|
||||||
this.onactive.emit({
|
this.onactive.emit({
|
||||||
|
@ -26,6 +27,7 @@ const uil = {
|
||||||
return this._active;
|
return this._active;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** @type {Layer} */
|
||||||
get layer() {
|
get layer() {
|
||||||
return this.active && this.active.layer;
|
return this.active && this.active.layer;
|
||||||
},
|
},
|
||||||
|
@ -112,9 +114,18 @@ const uil = {
|
||||||
"click",
|
"click",
|
||||||
(evn) => {
|
(evn) => {
|
||||||
evn.stopPropagation();
|
evn.stopPropagation();
|
||||||
commands.runCommand("deleteLayer", "Deleted Layer", {
|
commands.runCommand(
|
||||||
layer: uiLayer,
|
"deleteLayer",
|
||||||
});
|
"Deleted Layer",
|
||||||
|
{
|
||||||
|
layer: uiLayer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: `Deleted Layer ${uiLayer.name} [${uiLayer.id}]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{passive: false}
|
{passive: false}
|
||||||
);
|
);
|
||||||
|
@ -321,6 +332,118 @@ const uil = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class UILayer {
|
||||||
|
/** @type {string} Layer ID */
|
||||||
|
id;
|
||||||
|
|
||||||
|
/** @type {string} Display name of the layer */
|
||||||
|
name;
|
||||||
|
|
||||||
|
/** @type {Layer} Associated real layer */
|
||||||
|
layer;
|
||||||
|
|
||||||
|
/** @type {string} Custom key to access this layer */
|
||||||
|
key;
|
||||||
|
|
||||||
|
/** @type {string} The group the UI layer is on (for some categorization) */
|
||||||
|
group;
|
||||||
|
|
||||||
|
/** @type {boolean} If the layer displays the delete button */
|
||||||
|
deletable;
|
||||||
|
|
||||||
|
/** @type {HTMLElement} The entry element on the UI */
|
||||||
|
entry;
|
||||||
|
|
||||||
|
/** @type {boolean} [internal] Whether the layer is actually hidden right now */
|
||||||
|
_hidden;
|
||||||
|
|
||||||
|
/** @type {boolean} Whether the layer is hidden or not */
|
||||||
|
set hidden(v) {
|
||||||
|
if (v) {
|
||||||
|
this._hidden = true;
|
||||||
|
this.layer.hide(v);
|
||||||
|
this.entry && this.entry.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
this._hidden = false;
|
||||||
|
this.layer.unhide(v);
|
||||||
|
this.entry && this.entry.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get hidden() {
|
||||||
|
return this._hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {CanvasRenderingContext2D} */
|
||||||
|
get ctx() {
|
||||||
|
return this.layer.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
get canvas() {
|
||||||
|
return this.layer.canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new UI Layer
|
||||||
|
*
|
||||||
|
* @param {string} name Display name of the layer
|
||||||
|
* @param {object} extra
|
||||||
|
* @param {string} extra.id The id of the layer to create
|
||||||
|
* @param {string} extra.group The group the layer is on (for some categorization)
|
||||||
|
* @param {string} extra.key Custom key to access this layer
|
||||||
|
* @param {string} extra.deletable If the layer displays the delete button
|
||||||
|
*/
|
||||||
|
constructor(name, extra = {}) {
|
||||||
|
defaultOpt(extra, {
|
||||||
|
id: null,
|
||||||
|
group: null,
|
||||||
|
key: null,
|
||||||
|
deletable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.layer = imageCollection.registerLayer(extra.key, {
|
||||||
|
id: extra.id,
|
||||||
|
name,
|
||||||
|
category: "user",
|
||||||
|
after:
|
||||||
|
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
|
||||||
|
bgLayer,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.id = this.layer.id;
|
||||||
|
this.key = extra.key;
|
||||||
|
this.group = extra.group;
|
||||||
|
this.deletable = extra.deletable;
|
||||||
|
|
||||||
|
this.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register layer in uil
|
||||||
|
*/
|
||||||
|
register() {
|
||||||
|
uil.layers.push(this);
|
||||||
|
uil.layerIndex[this.id] = this;
|
||||||
|
uil.layerIndex[this.key] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes layer registration from uil
|
||||||
|
*/
|
||||||
|
unregister() {
|
||||||
|
const index = uil.layers.findIndex((v) => v === this);
|
||||||
|
|
||||||
|
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||||
|
|
||||||
|
if (uil.active === this)
|
||||||
|
uil.active = uil.layers[index + 1] || uil.layers[index - 1];
|
||||||
|
uil.layers.splice(index, 1);
|
||||||
|
uil.layerIndex[this.id] = undefined;
|
||||||
|
uil.layerIndex[this.key] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command for creating a new layer
|
* Command for creating a new layer
|
||||||
*/
|
*/
|
||||||
|
@ -329,61 +452,73 @@ commands.createCommand(
|
||||||
(title, opt, state) => {
|
(title, opt, state) => {
|
||||||
const options = Object.assign({}, opt) || {};
|
const options = Object.assign({}, opt) || {};
|
||||||
defaultOpt(options, {
|
defaultOpt(options, {
|
||||||
|
id: guid(),
|
||||||
group: null,
|
group: null,
|
||||||
name: "New Layer",
|
name: "New Layer",
|
||||||
|
key: null,
|
||||||
deletable: true,
|
deletable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!state.layer) {
|
if (!state.layer) {
|
||||||
const {group, name} = options;
|
let {id, name, group, key, deletable} = state;
|
||||||
|
|
||||||
const layer = imageCollection.registerLayer(null, {
|
if (!state.imported) {
|
||||||
name,
|
id = options.id;
|
||||||
category: "user",
|
name = options.name;
|
||||||
after:
|
group = options.group;
|
||||||
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
|
key = options.key;
|
||||||
bgLayer,
|
deletable = options.deletable;
|
||||||
|
|
||||||
|
state.name = name;
|
||||||
|
state.group = group;
|
||||||
|
state.key = key;
|
||||||
|
state.deletable = deletable;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.layer = new UILayer(name, {
|
||||||
|
id,
|
||||||
|
group,
|
||||||
|
key: key,
|
||||||
|
deletable: deletable,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.layer = {
|
if (state.hidden !== undefined) state.layer.hidden = state.hidden;
|
||||||
id: layer.id,
|
|
||||||
group,
|
state.id = state.layer.id;
|
||||||
name,
|
|
||||||
deletable: options.deletable,
|
|
||||||
_hidden: false,
|
|
||||||
set hidden(v) {
|
|
||||||
if (v) {
|
|
||||||
this._hidden = true;
|
|
||||||
this.layer.hide(v);
|
|
||||||
this.entry && this.entry.classList.add("hidden");
|
|
||||||
} else {
|
|
||||||
this._hidden = false;
|
|
||||||
this.layer.unhide(v);
|
|
||||||
this.entry && this.entry.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get hidden() {
|
|
||||||
return this._hidden;
|
|
||||||
},
|
|
||||||
entry: null,
|
|
||||||
layer,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
uil.layers.push(state.layer);
|
|
||||||
|
state.layer.register();
|
||||||
|
|
||||||
uil._syncLayers();
|
uil._syncLayers();
|
||||||
|
|
||||||
uil.active = state.layer;
|
uil.active = state.layer;
|
||||||
},
|
},
|
||||||
(title, state) => {
|
(title, state) => {
|
||||||
const index = uil.layers.findIndex((v) => v === state.layer);
|
state.layer.unregister();
|
||||||
|
|
||||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
|
||||||
|
|
||||||
if (uil.active === state.layer)
|
|
||||||
uil.active = uil.layers[index + 1] || uil.layers[index - 1];
|
|
||||||
uil.layers.splice(index, 1);
|
|
||||||
uil._syncLayers();
|
uil._syncLayers();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exportfn(state) {
|
||||||
|
return {
|
||||||
|
id: state.layer.id,
|
||||||
|
hidden: state.layer.hidden,
|
||||||
|
|
||||||
|
name: state.layer.name,
|
||||||
|
group: state.group,
|
||||||
|
key: state.key,
|
||||||
|
deletable: state.deletable,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
importfn(value, state) {
|
||||||
|
state.id = value.id;
|
||||||
|
state.hidden = value.hidden;
|
||||||
|
|
||||||
|
state.name = value.name;
|
||||||
|
state.group = value.group;
|
||||||
|
state.key = value.key;
|
||||||
|
state.deletable = value.deletable;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -424,6 +559,20 @@ commands.createCommand(
|
||||||
},
|
},
|
||||||
(title, state) => {
|
(title, state) => {
|
||||||
uil._moveLayerTo(state.layer, state.oldposition);
|
uil._moveLayerTo(state.layer, state.oldposition);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exportfn(state) {
|
||||||
|
return {
|
||||||
|
layer: state.layer.id,
|
||||||
|
position: state.position,
|
||||||
|
oldposition: state.oldposition,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
importfn(value, state) {
|
||||||
|
state.layer = uil.layerIndex[value.layer];
|
||||||
|
state.position = value.position;
|
||||||
|
state.oldposition = value.oldposition;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -470,6 +619,18 @@ commands.createCommand(
|
||||||
uil._syncLayers();
|
uil._syncLayers();
|
||||||
|
|
||||||
state.layer.hidden = false;
|
state.layer.hidden = false;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exportfn(state) {
|
||||||
|
return {
|
||||||
|
layer: state.layer.id,
|
||||||
|
position: state.position,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
importfn(value, state) {
|
||||||
|
state.layer = uil.layerIndex[value.layer];
|
||||||
|
state.position = value.position;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -485,27 +646,34 @@ commands.createCommand(
|
||||||
layerD: null,
|
layerD: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const layerS = options.layer || uil.active;
|
if (state.imported) {
|
||||||
|
state.layerS = uil.layerIndex[state.layerSID];
|
||||||
|
state.layerD = uil.layerIndex[state.layerDID];
|
||||||
|
}
|
||||||
|
|
||||||
if (!layerS.deletable)
|
if (!state.layerS) {
|
||||||
throw new TypeError(
|
const layerS = options.layer || uil.active;
|
||||||
"[layer.mergeLayer] Layer is a root layer and cannot be merged"
|
|
||||||
);
|
|
||||||
|
|
||||||
const index = uil.layers.indexOf(layerS);
|
if (!layerS.deletable)
|
||||||
if (index === -1)
|
throw new TypeError(
|
||||||
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
"[layer.mergeLayer] Layer is a undeletable layer and cannot be merged"
|
||||||
|
);
|
||||||
|
|
||||||
if (index === 0 && !options.layerD)
|
const index = uil.layers.indexOf(layerS);
|
||||||
throw new ReferenceError(
|
if (index === -1)
|
||||||
"[layer.mergeLayer] No layer below source layer exists"
|
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
||||||
);
|
|
||||||
|
|
||||||
// Use layer under source layer to merge into if not given
|
if (index === 0 && !options.layerD)
|
||||||
const layerD = options.layerD || uil.layers[index - 1];
|
throw new ReferenceError(
|
||||||
|
"[layer.mergeLayer] No layer below source layer exists"
|
||||||
|
);
|
||||||
|
|
||||||
state.layerS = layerS;
|
// Use layer under source layer to merge into if not given
|
||||||
state.layerD = layerD;
|
const layerD = options.layerD || uil.layers[index - 1];
|
||||||
|
|
||||||
|
state.layerS = layerS;
|
||||||
|
state.layerD = layerD;
|
||||||
|
}
|
||||||
|
|
||||||
// REFERENCE: This is a great reference for metacommands (commands that use other commands)
|
// REFERENCE: This is a great reference for metacommands (commands that use other commands)
|
||||||
// These commands should NOT record history as we are already executing a command
|
// These commands should NOT record history as we are already executing a command
|
||||||
|
@ -516,7 +684,7 @@ commands.createCommand(
|
||||||
image: state.layerS.layer.canvas,
|
image: state.layerS.layer.canvas,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
ctx: state.layerD.layer.ctx,
|
layer: state.layerD.layer,
|
||||||
},
|
},
|
||||||
{recordHistory: false}
|
{recordHistory: false}
|
||||||
);
|
);
|
||||||
|
@ -536,12 +704,22 @@ commands.createCommand(
|
||||||
state.drawCommand.redo();
|
state.drawCommand.redo();
|
||||||
state.delCommand.redo();
|
state.delCommand.redo();
|
||||||
},
|
},
|
||||||
|
exportfn(state) {
|
||||||
|
return {
|
||||||
|
layerS: state.layerS.id,
|
||||||
|
layerD: state.layerD.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
importfn(value, state) {
|
||||||
|
state.layerSID = value.layerS;
|
||||||
|
state.layerDID = value.layerD;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.runCommand(
|
commands.runCommand(
|
||||||
"addLayer",
|
"addLayer",
|
||||||
"Initial Layer Creation",
|
"Initial Layer Creation",
|
||||||
{name: "Default Image Layer", deletable: false},
|
{name: "Default Image Layer", key: "default", deletable: false},
|
||||||
{recordHistory: false}
|
{recordHistory: false}
|
||||||
);
|
);
|
||||||
|
|
|
@ -290,10 +290,19 @@ const colorBrushTool = () =>
|
||||||
const cropped = cropCanvas(canvas, {border: 10});
|
const cropped = cropCanvas(canvas, {border: 10});
|
||||||
const bb = cropped.bb;
|
const bb = cropped.bb;
|
||||||
|
|
||||||
commands.runCommand("drawImage", "Color Brush Draw", {
|
commands.runCommand(
|
||||||
image: cropped.canvas,
|
"drawImage",
|
||||||
...bb,
|
"Color Brush Draw",
|
||||||
});
|
{
|
||||||
|
image: cropped.canvas,
|
||||||
|
...bb,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: `Color brush drawn at x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
||||||
};
|
};
|
||||||
|
@ -339,10 +348,19 @@ const colorBrushTool = () =>
|
||||||
uil.layer.clear();
|
uil.layer.clear();
|
||||||
uil.ctx.drawImageRoot(bkpcanvas, 0, 0);
|
uil.ctx.drawImageRoot(bkpcanvas, 0, 0);
|
||||||
|
|
||||||
commands.runCommand("eraseImage", "Color Brush Erase", {
|
commands.runCommand(
|
||||||
mask: cropped.canvas,
|
"eraseImage",
|
||||||
...bb,
|
"Color Brush Erase",
|
||||||
});
|
{
|
||||||
|
mask: cropped.canvas,
|
||||||
|
...bb,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: `Color brush erase at x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,29 @@
|
||||||
* @typedef StableDiffusionRequest
|
* @typedef StableDiffusionRequest
|
||||||
* @property {string} prompt Stable Diffusion prompt
|
* @property {string} prompt Stable Diffusion prompt
|
||||||
* @property {string} negative_prompt Stable Diffusion negative prompt
|
* @property {string} negative_prompt Stable Diffusion negative prompt
|
||||||
|
*
|
||||||
|
* @property {number} width Stable Diffusion render width
|
||||||
|
* @property {number} height Stable Diffusion render height
|
||||||
|
*
|
||||||
|
* @property {number} n_iter Stable Diffusion number of iterations
|
||||||
|
* @property {number} batch_size Stable Diffusion images per batches
|
||||||
|
*
|
||||||
|
* @property {number} seed Stable Diffusion seed
|
||||||
|
* @property {number} steps Stable Diffusion step count
|
||||||
|
* @property {number} cfg_scale Stable Diffusion CFG scale
|
||||||
|
* @property {string} sampler_index Stable Diffusion sampler name
|
||||||
|
*
|
||||||
|
* @property {boolean} restore_faces WebUI face restoration
|
||||||
|
* @property {boolean} tiling WebUI tiling
|
||||||
|
* @property {string[]} styles WebUI styles
|
||||||
|
* @property {string} script_name WebUI script name
|
||||||
|
* @property {Array} script_args WebUI script args
|
||||||
|
*
|
||||||
|
* @property {string} mask Stable Diffusion mask (img2img)
|
||||||
|
* @property {number} mask_blur Stable Diffusion mask blur (img2img)
|
||||||
|
*
|
||||||
|
* @property {number} inpainting_fill Stable Diffusion inpainting fill (img2img)
|
||||||
|
* @property {boolean} inpaint_full_res Stable Diffusion full resolution (img2img)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -506,13 +506,42 @@ const _generate = async (endpoint, request, bb, options = {}) => {
|
||||||
ctx.drawImage(keepUnmaskCanvas, 0, 0);
|
ctx.drawImage(keepUnmaskCanvas, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.runCommand("drawImage", "Image Dream", {
|
let commandLog = "";
|
||||||
x: bb.x,
|
|
||||||
y: bb.y,
|
const addline = (v, newline = true) => {
|
||||||
w: bb.w,
|
commandLog += v;
|
||||||
h: bb.h,
|
if (newline) commandLog += "\n";
|
||||||
image: canvas,
|
};
|
||||||
});
|
|
||||||
|
addline(
|
||||||
|
`Dreamed image at x: ${bb.x}, y: ${bb.y}, w: ${bb.w}, h: ${bb.h}`
|
||||||
|
);
|
||||||
|
addline(` - resolution: (${request.width}, ${request.height})`);
|
||||||
|
addline(" - generation:");
|
||||||
|
addline(` + Seed = ${seeds[at]}`);
|
||||||
|
addline(` + Steps = ${request.steps}`);
|
||||||
|
addline(` + CFG = ${request.cfg_scale}`);
|
||||||
|
addline(` + Sampler = ${request.sampler_index}`);
|
||||||
|
addline(` + Model = ${modelAutoComplete.value}`);
|
||||||
|
addline(` + +Prompt = ${request.prompt}`);
|
||||||
|
addline(` + -Prompt = ${request.negative_prompt}`, false);
|
||||||
|
|
||||||
|
commands.runCommand(
|
||||||
|
"drawImage",
|
||||||
|
"Image Dream",
|
||||||
|
{
|
||||||
|
x: bb.x,
|
||||||
|
y: bb.y,
|
||||||
|
w: bb.w,
|
||||||
|
h: bb.h,
|
||||||
|
image: canvas,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: commandLog,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
clean(!toolbar._current_tool.state.preserveMasks);
|
clean(!toolbar._current_tool.state.preserveMasks);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1027,8 +1056,18 @@ const dream_generate_callback = async (bb, resolution, state) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erases an area from the canvas
|
||||||
|
*
|
||||||
|
* @param {BoundingBox} bb Bounding box of the area to be erased
|
||||||
|
*/
|
||||||
const dream_erase_callback = (bb) => {
|
const dream_erase_callback = (bb) => {
|
||||||
commands.runCommand("eraseImage", "Erase Area", bb);
|
commands.runCommand("eraseImage", "Erase Area", bb, {
|
||||||
|
extra: {
|
||||||
|
log: `Erased area at x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function applyOvermask(canvas, ctx, px) {
|
function applyOvermask(canvas, ctx, px) {
|
||||||
|
|
|
@ -248,6 +248,13 @@ const selectTransformTool = () =>
|
||||||
state.selected.position.x === state.original.sx &&
|
state.selected.position.x === state.original.sx &&
|
||||||
state.selected.position.y === state.original.sy &&
|
state.selected.position.y === state.original.sy &&
|
||||||
state.original.layer === uil.layer
|
state.original.layer === uil.layer
|
||||||
|
) &&
|
||||||
|
!isCanvasBlank(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
state.selected.canvas.width,
|
||||||
|
state.selected.canvas.height,
|
||||||
|
state.selected.canvas
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Put original image back
|
// Put original image back
|
||||||
|
@ -258,22 +265,61 @@ const selectTransformTool = () =>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Erase Original Selection Area
|
// Erase Original Selection Area
|
||||||
commands.runCommand("eraseImage", "Transform Tool Erase", {
|
commands.runCommand(
|
||||||
ctx: state.original.layer.ctx,
|
"eraseImage",
|
||||||
x: state.original.x,
|
"Transform Tool Erase",
|
||||||
y: state.original.y,
|
{
|
||||||
w: state.selected.canvas.width,
|
layer: state.original.layer,
|
||||||
h: state.selected.canvas.height,
|
x: state.original.x,
|
||||||
});
|
y: state.original.y,
|
||||||
|
w: state.selected.canvas.width,
|
||||||
|
h: state.selected.canvas.height,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: `Erased original selection area at x: ${state.original.x}, y: ${state.original.y}, width: ${state.selected.canvas.width}, height: ${state.selected.canvas.height} from layer ${state.original.layer.id}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Draw Image
|
// Draw Image
|
||||||
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
|
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
|
||||||
border: 10,
|
border: 10,
|
||||||
});
|
});
|
||||||
commands.runCommand("drawImage", "Transform Tool Apply", {
|
|
||||||
image: canvas,
|
let commandLog = "";
|
||||||
...bb,
|
const addline = (v, newline = true) => {
|
||||||
});
|
commandLog += v;
|
||||||
|
if (newline) commandLog += "\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
addline(
|
||||||
|
`Draw selected area to x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h} to layer ${state.original.layer.id}`
|
||||||
|
);
|
||||||
|
addline(
|
||||||
|
` - translation: (x: ${state.selected.position.x}, y: ${state.selected.position.y})`
|
||||||
|
);
|
||||||
|
addline(
|
||||||
|
` - rotation : ${
|
||||||
|
Math.round(1000 * ((180 * state.selected.rotation) / Math.PI)) /
|
||||||
|
1000
|
||||||
|
} degrees`,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
commands.runCommand(
|
||||||
|
"drawImage",
|
||||||
|
"Transform Tool Apply",
|
||||||
|
{
|
||||||
|
image: canvas,
|
||||||
|
...bb,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: commandLog,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
state.reset(true);
|
state.reset(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -454,7 +500,16 @@ const selectTransformTool = () =>
|
||||||
case "Delete":
|
case "Delete":
|
||||||
// Deletes selected area
|
// Deletes selected area
|
||||||
state.selected &&
|
state.selected &&
|
||||||
commands.runCommand("eraseImage", "Erase Area", state.selected);
|
commands.runCommand(
|
||||||
|
"eraseImage",
|
||||||
|
"Erase Area",
|
||||||
|
state.selected,
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: `[Placeholder] Delete selected area. TODO it's also broken`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
state.selected = null;
|
state.selected = null;
|
||||||
state.redraw();
|
state.redraw();
|
||||||
}
|
}
|
||||||
|
@ -526,7 +581,11 @@ const selectTransformTool = () =>
|
||||||
const aux = state.original;
|
const aux = state.original;
|
||||||
state.reset();
|
state.reset();
|
||||||
|
|
||||||
commands.runCommand("eraseImage", "Cut Image", aux);
|
commands.runCommand("eraseImage", "Cut Image", aux, {
|
||||||
|
extra: {
|
||||||
|
log: `Cut to clipboard a selected area at x: ${aux.x}, y: ${aux.y}, width: ${aux.w}, height: ${aux.h} from layer ${state.original.layer.id}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because firefox needs manual activation of the feature
|
// Because firefox needs manual activation of the feature
|
||||||
|
|
|
@ -146,13 +146,9 @@ const stampTool = () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
// Open IndexedDB connection
|
// Open IndexedDB connection
|
||||||
const IDBOpenRequest = window.indexedDB.open("stamp", 1);
|
|
||||||
|
|
||||||
// Synchronizes resources array with the DOM and Local Storage
|
// Synchronizes resources array with the DOM and Local Storage
|
||||||
const syncResources = () => {
|
const syncResources = () => {
|
||||||
// Saves to IndexedDB
|
// Saves to IndexedDB
|
||||||
/** @type {IDBDatabase} */
|
|
||||||
const db = state.stampDB;
|
|
||||||
const resources = db
|
const resources = db
|
||||||
.transaction("resources", "readwrite")
|
.transaction("resources", "readwrite")
|
||||||
.objectStore("resources");
|
.objectStore("resources");
|
||||||
|
@ -420,11 +416,38 @@ const stampTool = () =>
|
||||||
|
|
||||||
if (resource) {
|
if (resource) {
|
||||||
const {canvas, bb} = cropCanvas(ovCanvas, {border: 10});
|
const {canvas, bb} = cropCanvas(ovCanvas, {border: 10});
|
||||||
commands.runCommand("drawImage", "Image Stamp", {
|
|
||||||
image: canvas,
|
let commandLog = "";
|
||||||
x: bb.x,
|
|
||||||
y: bb.y,
|
const addline = (v, newline = true) => {
|
||||||
});
|
commandLog += v;
|
||||||
|
if (newline) commandLog += "\n";
|
||||||
|
};
|
||||||
|
addline(
|
||||||
|
`Stamped image '${resource.name}' to x: ${bb.x} and y: ${bb.y}`
|
||||||
|
);
|
||||||
|
addline(` - scaling : ${state.scale}`);
|
||||||
|
addline(
|
||||||
|
` - rotation: ${
|
||||||
|
Math.round(1000 * ((180 * rotation) / Math.PI)) / 1000
|
||||||
|
} degrees`,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
commands.runCommand(
|
||||||
|
"drawImage",
|
||||||
|
"Image Stamp",
|
||||||
|
{
|
||||||
|
image: canvas,
|
||||||
|
x: bb.x,
|
||||||
|
y: bb.y,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
log: commandLog,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (resource.temporary) {
|
if (resource.temporary) {
|
||||||
state.deleteResource(resource.id);
|
state.deleteResource(resource.id);
|
||||||
|
@ -563,35 +586,9 @@ const stampTool = () =>
|
||||||
state.ctxmenu.resourceList = resourceList;
|
state.ctxmenu.resourceList = resourceList;
|
||||||
|
|
||||||
// Performs resource fetch from IndexedDB
|
// Performs resource fetch from IndexedDB
|
||||||
|
const loadResources = async () => {
|
||||||
IDBOpenRequest.onerror = (e) => {
|
|
||||||
console.warn("[stamp] Failed to connect to IndexedDB");
|
|
||||||
console.warn(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
IDBOpenRequest.onupgradeneeded = (e) => {
|
|
||||||
const db = e.target.result;
|
|
||||||
|
|
||||||
console.debug(`[stamp] Setting up database version ${db.version}`);
|
|
||||||
|
|
||||||
const resourcesStore = db.createObjectStore("resources", {
|
|
||||||
keyPath: "id",
|
|
||||||
});
|
|
||||||
resourcesStore.createIndex("name", "name", {unique: false});
|
|
||||||
};
|
|
||||||
|
|
||||||
IDBOpenRequest.onsuccess = async (e) => {
|
|
||||||
console.debug("[stamp] Connected to IndexedDB");
|
console.debug("[stamp] Connected to IndexedDB");
|
||||||
|
|
||||||
state.stampDB = e.target.result;
|
|
||||||
|
|
||||||
state.stampDB.onerror = (evn) => {
|
|
||||||
console.warn(`[stamp] Database Error:`);
|
|
||||||
console.warn(evn.target.errorCode);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {IDBDatabase} */
|
|
||||||
const db = state.stampDB;
|
|
||||||
/** @type {IDBRequest<{id: string, name: string, src: string}[]>} */
|
/** @type {IDBRequest<{id: string, name: string, src: string}[]>} */
|
||||||
const FetchAllTransaction = db
|
const FetchAllTransaction = db
|
||||||
.transaction("resources")
|
.transaction("resources")
|
||||||
|
@ -621,6 +618,9 @@ const stampTool = () =>
|
||||||
syncResources();
|
syncResources();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (db) loadResources();
|
||||||
|
else ondatabaseload.on(loadResources);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
populateContextMenu: (menu, state) => {
|
populateContextMenu: (menu, state) => {
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>openOutpaint 🐠</title>
|
<title>openOutpaint 🐠</title>
|
||||||
<!-- CSS Variables -->
|
<!-- CSS Variables -->
|
||||||
<link href="../css/colors.css?v=3f81e80" rel="stylesheet" />
|
<link href="../css/colors.css?v=f732f19" rel="stylesheet" />
|
||||||
<link href="../css/icons.css?v=9ae0466" rel="stylesheet" />
|
<link href="../css/icons.css?v=466e14e" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="../css/index.css?v=882f400" rel="stylesheet" />
|
<link href="../css/index.css?v=61e08f5" rel="stylesheet" />
|
||||||
<link href="../css/layers.css?v=92c0352" rel="stylesheet" />
|
<link href="../css/layers.css?v=92c0352" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="../css/ui/generic.css?v=30837f8" rel="stylesheet" />
|
<link href="../css/ui/generic.css?v=30837f8" rel="stylesheet" />
|
||||||
|
|
6
res/icons/more-horizontal.svg
Normal file
6
res/icons/more-horizontal.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">
|
||||||
|
<circle cx="12" cy="12" r="1"></circle>
|
||||||
|
<circle cx="19" cy="12" r="1"></circle>
|
||||||
|
<circle cx="5" cy="12" r="1"></circle>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 314 B |
5
res/icons/pencil.svg
Normal file
5
res/icons/pencil.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="2" x2="22" y2="6"></line>
|
||||||
|
<path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 290 B |
6
res/icons/save.svg
Normal file
6
res/icons/save.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="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||||
|
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||||||
|
<polyline points="7 3 7 8 15 8"></polyline>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 374 B |
7
res/icons/scroll.svg
Normal file
7
res/icons/scroll.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<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="M10 17v2a2 2 0 0 1-2 2v0a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v3h3"></path>
|
||||||
|
<path d="M22 17v2a2 2 0 0 1-2 2H8"></path>
|
||||||
|
<path d="M19 17V5a2 2 0 0 0-2-2H4"></path>
|
||||||
|
<path d="M22 17H10"></path>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 404 B |
6
res/icons/upload.svg
Normal file
6
res/icons/upload.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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 345 B |
Loading…
Reference in a new issue