Merge branch 'main' into manual-scripts
# Conflicts: # index.html # js/ui/tool/dream.js
This commit is contained in:
commit
8eef3392ed
29 changed files with 1432 additions and 215 deletions
|
@ -1,5 +1,6 @@
|
|||
:root {
|
||||
--c-primary: #2c3333;
|
||||
--c-disabled: rgb(81, 81, 81);
|
||||
--c-hover: hsl(180, 7%, 30%);
|
||||
--c-active: hsl(180, 7%, 25%);
|
||||
--c-primary-accent: hsl(180, 7%, 40%);
|
||||
|
|
|
@ -71,6 +71,11 @@
|
|||
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 {
|
||||
-webkit-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");
|
||||
}
|
||||
|
||||
.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.icon > .icon-expand {
|
||||
-webkit-mask-image: url("../res/icons/expand.svg");
|
||||
|
|
|
@ -227,11 +227,25 @@ body {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.host-field-wrapper input {
|
||||
flex-shrink: 0;
|
||||
.host-field-wrapper > .host-field {
|
||||
display: flex;
|
||||
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;
|
||||
min-width: 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" />
|
||||
<title>openOutpaint 🐠</title>
|
||||
<!-- CSS Variables -->
|
||||
<link href="css/colors.css?v=3f81e80" rel="stylesheet" />
|
||||
<link href="css/icons.css?v=9ae0466" rel="stylesheet" />
|
||||
<link href="css/colors.css?v=f732f19" 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/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/layers.css?v=1d66c2b" rel="stylesheet" />
|
||||
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
||||
|
@ -38,17 +39,57 @@
|
|||
</button>
|
||||
</div>
|
||||
<div id="info" class="menu-container" style="min-width: 200px">
|
||||
<label>
|
||||
Host
|
||||
<div class="host-field-wrapper">
|
||||
<input id="host" value="http://127.0.0.1:7860" />
|
||||
<div
|
||||
id="workspace-select-area"
|
||||
style="display: flex; margin-bottom: 5px">
|
||||
<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
|
||||
id="connection-status-indicator"
|
||||
class="connection-status before">
|
||||
<span id="connection-status-indicator-text">Waiting</span>
|
||||
id="more-workspace-menu"
|
||||
class="workspace-collapsible collapsed">
|
||||
<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 style="width: 5px; background-color: var(--c-primary)"></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 -->
|
||||
<button type="button" class="collapsible">Prompts</button>
|
||||
|
@ -88,7 +129,7 @@
|
|||
<button id="refreshModelsBtn" onclick="getModels(true)">
|
||||
<img
|
||||
class="refreshbutton"
|
||||
src="./res/icons/refresh-cw.svg"
|
||||
src="./res/icons/refresh-cw.svg?v=f627140"
|
||||
alt="refresh models"
|
||||
title="refresh models" />
|
||||
</button>
|
||||
|
@ -100,7 +141,7 @@
|
|||
type="number"
|
||||
id="seed"
|
||||
onchange="changeSeed()"
|
||||
min="1"
|
||||
min="-1"
|
||||
max="9999999999"
|
||||
value="-1"
|
||||
step="1" />
|
||||
|
@ -242,7 +283,16 @@
|
|||
|
||||
<!-- History -->
|
||||
<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 id="history" class="history"></div>
|
||||
<div class="button-array" style="padding: 10px">
|
||||
|
@ -375,22 +425,27 @@
|
|||
<div class="ui separator"></div>
|
||||
<iframe
|
||||
id="page-overlay"
|
||||
src="pages/configuration.html?v=7fca00b"></iframe>
|
||||
src="pages/configuration.html?v=fdbd833"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basics -->
|
||||
<script src="js/global.js?v=ac30d16" type="text/javascript"></script>
|
||||
<script src="js/defaults.js?v=5b06818" type="text/javascript"></script>
|
||||
|
||||
<!-- Base Libs -->
|
||||
<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/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=769485c" type="text/javascript"></script>
|
||||
<script src="js/lib/layers.js?v=a1f8aea" type="text/javascript"></script>
|
||||
<script src="js/lib/commands.js?v=bf23c83" type="text/javascript"></script>
|
||||
<script src="js/lib/layers.js?v=1a452a1" 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/ui.js?v=fe9b702" type="text/javascript"></script>
|
||||
<script src="js/lib/ui.js?v=17014b3" type="text/javascript"></script>
|
||||
|
||||
<script
|
||||
src="js/initalize/layers.populate.js?v=066dc8e"
|
||||
|
@ -402,13 +457,13 @@
|
|||
|
||||
<!-- Content -->
|
||||
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
||||
<script src="js/index.js?v=04d2bb0" type="text/javascript"></script>
|
||||
<script src="js/index.js?v=b1402ee" type="text/javascript"></script>
|
||||
|
||||
<script
|
||||
src="js/ui/floating/history.js?v=fc92d14"
|
||||
src="js/ui/floating/history.js?v=4f29db4"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/floating/layers.js?v=8e66543"
|
||||
src="js/ui/floating/layers.js?v=aa78ec8"
|
||||
type="text/javascript"></script>
|
||||
|
||||
<!-- Load Tools -->
|
||||
|
@ -416,22 +471,25 @@
|
|||
src="js/ui/tool/generic.js?v=3e678e0"
|
||||
type="text/javascript"></script>
|
||||
|
||||
<script src="js/ui/tool/dream.js?v=07abfe8" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/dream.js?v=d1ba895" type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/maskbrush.js?v=d88810f"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/colorbrush.js?v=6f1d2f4"
|
||||
src="js/ui/tool/colorbrush.js?v=46011ee"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/select.js?v=f290e83"
|
||||
src="js/ui/tool/select.js?v=63fe672"
|
||||
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=455f9fe" type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/interrogate.js?v=e579ff1"
|
||||
type="text/javascript"></script>
|
||||
|
||||
<!-- Initialize -->
|
||||
<script
|
||||
src="js/initalize/workspace.populate.js?v=e5586da"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/initalize/shortcuts.populate.js?v=e68546f"
|
||||
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
|
||||
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() {
|
||||
testHostConfiguration();
|
||||
loadSettings();
|
||||
|
@ -210,8 +223,6 @@ function testHostConfiguration() {
|
|||
host = value;
|
||||
hostEl.value = host;
|
||||
localStorage.setItem("openoutpaint/host", host);
|
||||
|
||||
testHostConfiguration();
|
||||
};
|
||||
|
||||
const current = localStorage.getItem("openoutpaint/host");
|
||||
|
@ -487,10 +498,19 @@ async function testHostConnection() {
|
|||
function newImage(evt) {
|
||||
clearPaintedMask();
|
||||
uil.layers.forEach(({layer}) => {
|
||||
commands.runCommand("eraseImage", "Clear Canvas", {
|
||||
...layer.bb,
|
||||
ctx: layer.ctx,
|
||||
});
|
||||
commands.runCommand(
|
||||
"eraseImage",
|
||||
"Clear Canvas",
|
||||
{
|
||||
...layer.bb,
|
||||
ctx: layer.ctx,
|
||||
},
|
||||
{
|
||||
extra: {
|
||||
log: `Cleared Canvas`,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -567,16 +587,16 @@ const makeSlider = (
|
|||
textStep = null,
|
||||
valuecb = null
|
||||
) => {
|
||||
const local = lsKey && localStorage.getItem(`openoutpaint/${lsKey}`);
|
||||
const local = lsKey && workspaces.current.settings[lsKey];
|
||||
const def = parseFloat(local === null ? defaultValue : local);
|
||||
let cb = (v) => {
|
||||
stableDiffusionData[lsKey] = v;
|
||||
if (lsKey) localStorage.setItem(`openoutpaint/${lsKey}`, v);
|
||||
if (lsKey) workspaces.current.settings[lsKey] = v;
|
||||
};
|
||||
if (valuecb) {
|
||||
cb = (v) => {
|
||||
valuecb(v);
|
||||
localStorage.setItem(`openoutpaint/${lsKey}`, v);
|
||||
if (lsKey) workspaces.current.settings[lsKey] = v;
|
||||
};
|
||||
}
|
||||
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() {
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/upscalers";
|
||||
let upscalers = [];
|
||||
|
@ -1167,11 +1284,6 @@ function loadSettings() {
|
|||
localStorage.getItem("openoutpaint/mask_blur") == null
|
||||
? 0
|
||||
: localStorage.getItem("openoutpaint/mask_blur");
|
||||
var _seed =
|
||||
localStorage.getItem("openoutpaint/seed") == null
|
||||
? -1
|
||||
: localStorage.getItem("openoutpaint/seed");
|
||||
|
||||
let _enable_hr =
|
||||
localStorage.getItem("openoutpaint/enable_hr") === null
|
||||
? false
|
||||
|
@ -1202,7 +1314,7 @@ function loadSettings() {
|
|||
|
||||
// set the values into the UI
|
||||
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("cbxRestoreFaces").checked = Boolean(_restore_faces);
|
||||
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 {() => 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 {() => 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}} extra Extra information saved with the command
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -14,6 +16,7 @@
|
|||
*
|
||||
* @typedef CommandExtraParams
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
@ -74,31 +112,57 @@ const commands = makeReadOnly(
|
|||
* @param {string} name Command identifier (name)
|
||||
* @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 {object} options Extra options
|
||||
* @param {CommandDoCallback} options.redo A method that redoes the action after undone (default: run)
|
||||
* @param {object} opt Extra options
|
||||
* @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}
|
||||
*/
|
||||
createCommand(name, run, undo, options = {}) {
|
||||
defaultOpt(options, {
|
||||
createCommand(name, run, undo, opt = {}) {
|
||||
defaultOpt(opt, {
|
||||
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
|
||||
const copy = {};
|
||||
Object.assign(copy, options);
|
||||
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} */
|
||||
const entry = {
|
||||
id: guid(),
|
||||
title,
|
||||
state,
|
||||
async export() {
|
||||
return {
|
||||
command: name,
|
||||
title,
|
||||
data: await exportfn(state),
|
||||
};
|
||||
},
|
||||
extra: extra.extra,
|
||||
};
|
||||
|
||||
if (extra.importData) {
|
||||
await importfn(extra.importData, state);
|
||||
state.imported = extra.importData;
|
||||
}
|
||||
|
||||
// Attempt to run command
|
||||
try {
|
||||
console.debug(`[commands] Running '${title}'[${name}]`);
|
||||
|
@ -209,47 +273,63 @@ commands.createCommand(
|
|||
"drawImage",
|
||||
(title, options, state) => {
|
||||
if (
|
||||
!options ||
|
||||
options.image === undefined ||
|
||||
options.x === undefined ||
|
||||
options.y === undefined
|
||||
!state.imported &&
|
||||
(!options ||
|
||||
options.image === 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
|
||||
if (!state.context) {
|
||||
const context = options.ctx || uil.ctx;
|
||||
state.context = context;
|
||||
if (!state.layer) {
|
||||
/** @type {Layer} */
|
||||
let layer = options.layer;
|
||||
if (!options.layer && state.layerId)
|
||||
layer = imageCollection.layers[state.layerId];
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
const imgData = context.getImageData(
|
||||
options.x,
|
||||
options.y,
|
||||
options.w || options.image.width,
|
||||
options.h || options.image.height
|
||||
);
|
||||
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 = new Image();
|
||||
state.original.src = cutout.toDataURL();
|
||||
if (!options.layer && !state.layerId) layer = uil.layer;
|
||||
|
||||
state.layer = layer;
|
||||
state.context = layer.ctx;
|
||||
|
||||
if (!state.imported) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = options.image.width;
|
||||
canvas.height = options.image.height;
|
||||
canvas.getContext("2d").drawImage(options.image, 0, 0);
|
||||
|
||||
state.image = canvas;
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
const imgData = state.context.getImageData(
|
||||
options.x,
|
||||
options.y,
|
||||
options.w || options.image.width,
|
||||
options.h || options.image.height
|
||||
);
|
||||
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
|
||||
state.context.drawImage(
|
||||
options.image,
|
||||
state.image,
|
||||
0,
|
||||
0,
|
||||
options.image.width,
|
||||
options.image.height,
|
||||
state.image.width,
|
||||
state.image.height,
|
||||
state.box.x,
|
||||
state.box.y,
|
||||
state.box.w,
|
||||
|
@ -261,6 +341,51 @@ commands.createCommand(
|
|||
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);
|
||||
},
|
||||
{
|
||||
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",
|
||||
(title, options, state) => {
|
||||
if (
|
||||
!options ||
|
||||
options.x === undefined ||
|
||||
options.y === undefined ||
|
||||
options.w === undefined ||
|
||||
options.h === undefined
|
||||
!state.imported &&
|
||||
(!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?}";
|
||||
|
||||
if (state.imported) {
|
||||
state.layer = imageCollection.layers[state.layerId];
|
||||
state.context = state.layer.ctx;
|
||||
}
|
||||
|
||||
// Check if we have state
|
||||
if (!state.context) {
|
||||
const context = options.ctx || uil.ctx;
|
||||
state.context = context;
|
||||
if (!state.layer) {
|
||||
const layer = (options.layer || state.layerId) ?? uil.layer;
|
||||
state.layer = layer;
|
||||
state.mask = options.mask;
|
||||
state.context = layer.ctx;
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
state.box = {
|
||||
|
@ -295,7 +428,7 @@ commands.createCommand(
|
|||
cutout
|
||||
.getContext("2d")
|
||||
.drawImage(
|
||||
context.canvas,
|
||||
state.context.canvas,
|
||||
options.x,
|
||||
options.y,
|
||||
options.w,
|
||||
|
@ -316,9 +449,9 @@ commands.createCommand(
|
|||
const op = state.context.globalCompositeOperation;
|
||||
state.context.globalCompositeOperation = "destination-out";
|
||||
|
||||
if (options.mask)
|
||||
if (state.mask)
|
||||
state.context.drawImage(
|
||||
options.mask,
|
||||
state.mask,
|
||||
state.box.x,
|
||||
state.box.y,
|
||||
state.box.w,
|
||||
|
@ -340,5 +473,59 @@ commands.createCommand(
|
|||
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);
|
||||
},
|
||||
{
|
||||
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} 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 {{[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} resolution The resolution of the collection (canvas)
|
||||
* @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 {object} options
|
||||
* @param {string} options.id
|
||||
* @param {string} options.name
|
||||
* @param {?BoundingBox} options.bb
|
||||
* @param {string} [options.category]
|
||||
|
@ -346,9 +347,12 @@ const layers = {
|
|||
*/
|
||||
registerLayer(key = null, options = {}) {
|
||||
// Make ID
|
||||
const id = guid();
|
||||
const id = options.id ?? guid();
|
||||
|
||||
defaultOpt(options, {
|
||||
// ID of the layer
|
||||
id: null,
|
||||
|
||||
// Display name for the layer
|
||||
name: key || `Temporary ${id}`,
|
||||
|
||||
|
@ -618,6 +622,7 @@ const layers = {
|
|||
collection._layers.splice(index, 0, layer);
|
||||
}
|
||||
if (key) collection.layers[key] = layer;
|
||||
collection.layers[id] = layer;
|
||||
|
||||
if (key === null)
|
||||
console.debug(
|
||||
|
@ -651,7 +656,8 @@ const layers = {
|
|||
layers.listen.onlayerdelete.emit({
|
||||
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);
|
||||
|
||||
|
|
|
@ -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 {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
|
||||
* @returns {AutoCompleteElement}
|
||||
*/
|
||||
function createAutoComplete(
|
||||
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 makeHistoryEntry = (index, id, title) => {
|
||||
|
@ -25,7 +41,7 @@
|
|||
};
|
||||
|
||||
_commands_events.on((message) => {
|
||||
if (message.action === "run") {
|
||||
if (message.action === "run" || message.action === "clear") {
|
||||
Array.from(historyView.children).forEach((child) => {
|
||||
if (
|
||||
!commands._history.find((entry) => `hist-${entry.id}` === child.id)
|
||||
|
|
|
@ -8,6 +8,7 @@ const uil = {
|
|||
|
||||
_ui_layer_list: document.getElementById("layer-list"),
|
||||
layers: [],
|
||||
layerIndex: {},
|
||||
_active: null,
|
||||
set active(v) {
|
||||
this.onactive.emit({
|
||||
|
@ -26,6 +27,7 @@ const uil = {
|
|||
return this._active;
|
||||
},
|
||||
|
||||
/** @type {Layer} */
|
||||
get layer() {
|
||||
return this.active && this.active.layer;
|
||||
},
|
||||
|
@ -112,9 +114,18 @@ const uil = {
|
|||
"click",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
commands.runCommand("deleteLayer", "Deleted Layer", {
|
||||
layer: uiLayer,
|
||||
});
|
||||
commands.runCommand(
|
||||
"deleteLayer",
|
||||
"Deleted Layer",
|
||||
{
|
||||
layer: uiLayer,
|
||||
},
|
||||
{
|
||||
extra: {
|
||||
log: `Deleted Layer ${uiLayer.name} [${uiLayer.id}]`,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
{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
|
||||
*/
|
||||
|
@ -329,61 +452,73 @@ commands.createCommand(
|
|||
(title, opt, state) => {
|
||||
const options = Object.assign({}, opt) || {};
|
||||
defaultOpt(options, {
|
||||
id: guid(),
|
||||
group: null,
|
||||
name: "New Layer",
|
||||
key: null,
|
||||
deletable: true,
|
||||
});
|
||||
|
||||
if (!state.layer) {
|
||||
const {group, name} = options;
|
||||
let {id, name, group, key, deletable} = state;
|
||||
|
||||
const layer = imageCollection.registerLayer(null, {
|
||||
name,
|
||||
category: "user",
|
||||
after:
|
||||
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
|
||||
bgLayer,
|
||||
if (!state.imported) {
|
||||
id = options.id;
|
||||
name = options.name;
|
||||
group = options.group;
|
||||
key = options.key;
|
||||
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 = {
|
||||
id: layer.id,
|
||||
group,
|
||||
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,
|
||||
};
|
||||
if (state.hidden !== undefined) state.layer.hidden = state.hidden;
|
||||
|
||||
state.id = state.layer.id;
|
||||
}
|
||||
uil.layers.push(state.layer);
|
||||
|
||||
state.layer.register();
|
||||
|
||||
uil._syncLayers();
|
||||
|
||||
uil.active = state.layer;
|
||||
},
|
||||
(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();
|
||||
},
|
||||
{
|
||||
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) => {
|
||||
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();
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const layerS = options.layer || uil.active;
|
||||
if (state.imported) {
|
||||
state.layerS = uil.layerIndex[state.layerSID];
|
||||
state.layerD = uil.layerIndex[state.layerDID];
|
||||
}
|
||||
|
||||
if (!layerS.deletable)
|
||||
throw new TypeError(
|
||||
"[layer.mergeLayer] Layer is a root layer and cannot be merged"
|
||||
);
|
||||
if (!state.layerS) {
|
||||
const layerS = options.layer || uil.active;
|
||||
|
||||
const index = uil.layers.indexOf(layerS);
|
||||
if (index === -1)
|
||||
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
||||
if (!layerS.deletable)
|
||||
throw new TypeError(
|
||||
"[layer.mergeLayer] Layer is a undeletable layer and cannot be merged"
|
||||
);
|
||||
|
||||
if (index === 0 && !options.layerD)
|
||||
throw new ReferenceError(
|
||||
"[layer.mergeLayer] No layer below source layer exists"
|
||||
);
|
||||
const index = uil.layers.indexOf(layerS);
|
||||
if (index === -1)
|
||||
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
||||
|
||||
// Use layer under source layer to merge into if not given
|
||||
const layerD = options.layerD || uil.layers[index - 1];
|
||||
if (index === 0 && !options.layerD)
|
||||
throw new ReferenceError(
|
||||
"[layer.mergeLayer] No layer below source layer exists"
|
||||
);
|
||||
|
||||
state.layerS = layerS;
|
||||
state.layerD = layerD;
|
||||
// Use layer under source layer to merge into if not given
|
||||
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)
|
||||
// These commands should NOT record history as we are already executing a command
|
||||
|
@ -516,7 +684,7 @@ commands.createCommand(
|
|||
image: state.layerS.layer.canvas,
|
||||
x: 0,
|
||||
y: 0,
|
||||
ctx: state.layerD.layer.ctx,
|
||||
layer: state.layerD.layer,
|
||||
},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
@ -536,12 +704,22 @@ commands.createCommand(
|
|||
state.drawCommand.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(
|
||||
"addLayer",
|
||||
"Initial Layer Creation",
|
||||
{name: "Default Image Layer", deletable: false},
|
||||
{name: "Default Image Layer", key: "default", deletable: false},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
|
|
@ -290,10 +290,19 @@ const colorBrushTool = () =>
|
|||
const cropped = cropCanvas(canvas, {border: 10});
|
||||
const bb = cropped.bb;
|
||||
|
||||
commands.runCommand("drawImage", "Color Brush Draw", {
|
||||
image: cropped.canvas,
|
||||
...bb,
|
||||
});
|
||||
commands.runCommand(
|
||||
"drawImage",
|
||||
"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);
|
||||
};
|
||||
|
@ -339,10 +348,19 @@ const colorBrushTool = () =>
|
|||
uil.layer.clear();
|
||||
uil.ctx.drawImageRoot(bkpcanvas, 0, 0);
|
||||
|
||||
commands.runCommand("eraseImage", "Color Brush Erase", {
|
||||
mask: cropped.canvas,
|
||||
...bb,
|
||||
});
|
||||
commands.runCommand(
|
||||
"eraseImage",
|
||||
"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);
|
||||
};
|
||||
|
|
|
@ -4,6 +4,29 @@
|
|||
* @typedef StableDiffusionRequest
|
||||
* @property {string} prompt Stable Diffusion 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)
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -526,6 +526,42 @@ const _generate = async (endpoint, request, bb, options = {}) => {
|
|||
h: bb.h,
|
||||
image: canvas,
|
||||
});
|
||||
let commandLog = "";
|
||||
|
||||
const addline = (v, newline = true) => {
|
||||
commandLog += v;
|
||||
if (newline) commandLog += "\n";
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
@ -1040,8 +1076,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) => {
|
||||
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) {
|
||||
|
|
|
@ -248,6 +248,13 @@ const selectTransformTool = () =>
|
|||
state.selected.position.x === state.original.sx &&
|
||||
state.selected.position.y === state.original.sy &&
|
||||
state.original.layer === uil.layer
|
||||
) &&
|
||||
!isCanvasBlank(
|
||||
0,
|
||||
0,
|
||||
state.selected.canvas.width,
|
||||
state.selected.canvas.height,
|
||||
state.selected.canvas
|
||||
)
|
||||
) {
|
||||
// Put original image back
|
||||
|
@ -258,22 +265,61 @@ const selectTransformTool = () =>
|
|||
);
|
||||
|
||||
// Erase Original Selection Area
|
||||
commands.runCommand("eraseImage", "Transform Tool Erase", {
|
||||
ctx: state.original.layer.ctx,
|
||||
x: state.original.x,
|
||||
y: state.original.y,
|
||||
w: state.selected.canvas.width,
|
||||
h: state.selected.canvas.height,
|
||||
});
|
||||
commands.runCommand(
|
||||
"eraseImage",
|
||||
"Transform Tool Erase",
|
||||
{
|
||||
layer: state.original.layer,
|
||||
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
|
||||
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
|
||||
border: 10,
|
||||
});
|
||||
commands.runCommand("drawImage", "Transform Tool Apply", {
|
||||
image: canvas,
|
||||
...bb,
|
||||
});
|
||||
|
||||
let commandLog = "";
|
||||
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);
|
||||
} else {
|
||||
|
@ -454,7 +500,16 @@ const selectTransformTool = () =>
|
|||
case "Delete":
|
||||
// Deletes selected area
|
||||
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.redraw();
|
||||
}
|
||||
|
@ -526,7 +581,11 @@ const selectTransformTool = () =>
|
|||
const aux = state.original;
|
||||
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
|
||||
|
|
|
@ -146,13 +146,9 @@ const stampTool = () =>
|
|||
};
|
||||
|
||||
// Open IndexedDB connection
|
||||
const IDBOpenRequest = window.indexedDB.open("stamp", 1);
|
||||
|
||||
// Synchronizes resources array with the DOM and Local Storage
|
||||
const syncResources = () => {
|
||||
// Saves to IndexedDB
|
||||
/** @type {IDBDatabase} */
|
||||
const db = state.stampDB;
|
||||
const resources = db
|
||||
.transaction("resources", "readwrite")
|
||||
.objectStore("resources");
|
||||
|
@ -425,11 +421,38 @@ const stampTool = () =>
|
|||
commands.runCommand("addLayer", "Added Layer", {});
|
||||
}
|
||||
const {canvas, bb} = cropCanvas(ovCanvas, {border: 10});
|
||||
commands.runCommand("drawImage", "Image Stamp", {
|
||||
image: canvas,
|
||||
x: bb.x,
|
||||
y: bb.y,
|
||||
});
|
||||
|
||||
let commandLog = "";
|
||||
|
||||
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) {
|
||||
state.deleteResource(resource.id);
|
||||
|
@ -568,35 +591,9 @@ const stampTool = () =>
|
|||
state.ctxmenu.resourceList = resourceList;
|
||||
|
||||
// Performs resource fetch from IndexedDB
|
||||
|
||||
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) => {
|
||||
const loadResources = async () => {
|
||||
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}[]>} */
|
||||
const FetchAllTransaction = db
|
||||
.transaction("resources")
|
||||
|
@ -626,6 +623,9 @@ const stampTool = () =>
|
|||
syncResources();
|
||||
};
|
||||
};
|
||||
|
||||
if (db) loadResources();
|
||||
else ondatabaseload.on(loadResources);
|
||||
}
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
<meta charset="utf-8" />
|
||||
<title>openOutpaint 🐠</title>
|
||||
<!-- CSS Variables -->
|
||||
<link href="../css/colors.css?v=3f81e80" rel="stylesheet" />
|
||||
<link href="../css/icons.css?v=9ae0466" rel="stylesheet" />
|
||||
<link href="../css/colors.css?v=f732f19" 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/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