Merge pull request #64 from zero01101/bleeding-edge

A fun tool for painting things
This commit is contained in:
Victor Seiji Hariki 2022-12-03 13:22:31 -03:00 committed by GitHub
commit 9b174d66c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1295 additions and 354 deletions

View file

@ -13,8 +13,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Prettify
uses: creyD/prettier_action@v4.2
with:
ref: ${{ github.head_ref }}
prettier_options: --write **/*.{js,html,css,md}

View file

@ -1,6 +1,12 @@
* {
font-size: 100%;
font-family: Arial, Helvetica, sans-serif;
user-select: none;
}
input,
textarea {
user-select: auto;
}
/* Body is stuck with no scroll */
@ -136,7 +142,7 @@ body {
color: #fff;
}
#models {
.wideSelect {
width: 100%;
text-overflow: ellipsis;
}
@ -146,59 +152,83 @@ body {
position: relative;
display: flex;
align-items: center;
align-items: stretch;
justify-content: space-between;
width: 100%;
height: fit-content;
}
.host-field-wrapper input {
flex-shrink: 0;
width: calc(100% - 15px);
display: block;
border: 0;
}
.host-field-wrapper .connection-status {
width: 15px;
height: 15px;
position: absolute;
left: calc(100% - 15px);
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
box-sizing: inherit;
border-radius: 50%;
margin: 5px;
cursor: pointer;
aspect-ratio: 1;
transition-duration: 50ms;
padding-top: 1px;
padding-bottom: 1px;
overflow: hidden;
}
.host-field-wrapper .connection-status:active,
.host-field-wrapper .connection-status:hover {
width: 19px;
height: 19px;
width: fit-content;
padding-left: 5px;
padding-right: 6px;
margin: 3px;
filter: brightness(110%);
}
.host-field-wrapper .connection-status:active {
width: 17px;
height: 17px;
filter: brightness(80%);
}
margin: 4px;
.host-field-wrapper .connection-status > #connection-status-indicator-text {
opacity: 0%;
transition-duration: 20ms;
}
.host-field-wrapper
.connection-status:hover
> #connection-status-indicator-text {
opacity: 100%;
}
.host-field-wrapper .connection-status.online {
background-color: #49dd49;
color: #1f3f1f;
}
.host-field-wrapper .connection-status.offline {
background-color: #dd4949;
color: #3f1f1f;
}
.host-field-wrapper .connection-status.cors-issue {
background-color: #dddd49;
color: #3f3f1f;
}
.host-field-wrapper .connection-status.before {
background-color: #777;
color: #1f1f1f;
}
input#host {
@ -222,6 +252,19 @@ div.prompt-wrapper > textarea:focus {
width: 700px;
}
/* Style Field */
select > .style-select-option {
cursor: pointer;
}
select > .style-select-option:hover {
background-color: #999;
}
select > .style-select-option:active {
background-color: #aaa;
}
/* Tool buttons */
.button-array {
display: flex;

View file

@ -90,8 +90,26 @@ div.slider-wrapper > input.text {
appearance: textfield;
border: 0px;
padding-top: 5px;
height: 15px;
height: 100%;
text-align: center;
background-color: transparent;
}
/* Select Input */
select > option:checked::after {
content: "";
position: absolute;
right: 5px;
top: 0;
height: 100%;
aspect-ratio: 1;
background-color: darkgreen;
-webkit-mask-image: url("/res/icons/check.svg");
-webkit-mask-size: contain;
mask-image: url("/res/icons/check.svg");
mask-size: contain;
}

View file

@ -38,14 +38,69 @@
}
.resource-manager > .resource-list > * {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-manager > .resource-list > * > .resource-title {
overflow: hidden;
margin: 5px;
text-overflow: ellipsis;
}
.resource-manager > .resource-list > * > .actions {
display: flex;
align-items: center;
}
.resource-manager .actions > button {
display: flex;
align-items: stretch;
padding: 0;
width: 30px;
aspect-ratio: 1;
background-color: transparent;
border: 0;
cursor: pointer;
}
.resource-manager .actions > button:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.resource-manager .actions > button:active {
background-color: rgba(255, 255, 255, 0.7);
}
.resource-manager .actions > button > *:first-child {
flex: 1;
margin: 3px;
-webkit-mask-size: contain;
mask-size: contain;
background-color: var(--c-primary);
}
.resource-manager .actions > .rename-btn > *:first-child {
-webkit-mask-image: url("/res/icons/edit.svg");
mask-image: url("/res/icons/edit.svg");
}
.resource-manager .actions > .delete-btn > *:first-child {
-webkit-mask-image: url("/res/icons/trash.svg");
mask-image: url("/res/icons/trash.svg");
}
.resource-manager > .resource-list > .selected:hover,
.resource-manager > .resource-list > *:hover {
background-color: #ffff;
background-color: #fff8;
}
.resource-manager > .resource-list > .selected {
background-color: #fff6;

View file

@ -43,8 +43,6 @@
padding: 0;
background-color: var(--c-text);
right: 2px;
top: 10px;
@ -78,6 +76,8 @@
border-radius: 5px;
cursor: pointer;
transition-duration: 50ms;
}
#ui-toolbar .tool.using {
@ -87,3 +87,8 @@
#ui-toolbar .tool:hover {
background-color: var(--c-hover);
}
#ui-toolbar .tool:active {
background-color: var(--c-hover);
filter: brightness(120%);
}

View file

@ -36,7 +36,9 @@
<input id="host" value="http://127.0.0.1:7860" />
<div
id="connection-status-indicator"
class="connection-status"></div>
class="connection-status before">
<span id="connection-status-indicator-text">Waiting</span>
</div>
</div>
</label>
<!-- Prompts section -->
@ -51,6 +53,12 @@
<div class="prompt-wrapper">
<textarea id="negPrompt"></textarea>
</div>
<label for="styleSelect">Styles:</label>
<select
id="styleSelect"
class="wideSelect"
onchange="changeStyles()"
multiple></select>
<!-- <hr /> -->
</div>
<!-- SD section -->
@ -59,7 +67,10 @@
</button>
<div class="content">
<label for="models">Model:</label>
<select id="models" onchange="changeModel()"></select>
<select
id="models"
class="wideSelect"
onchange="changeModel()"></select>
<br />
<label for="samplerSelect">Sampler:</label>
<select id="samplerSelect" onchange="changeSampler()"></select>
@ -75,16 +86,15 @@
value="-1"
step="1" />
<br />
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()" />
<label for="cbxHRFix">Auto txt2img HRfix</label>
<div id="resolution"></div>
<div id="steps"></div>
<div id="cfgScale"></div>
<div id="batchSize"></div>
<div id="batchCount"></div>
</div>
<!-- Unsectioned -->
<div id="scaleFactor"></div>
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()" />
<label for="cbxHRFix">Auto txt2img HRfix</label>
<br />
<label for="maskBlur">Mask blur:</label>
<span id="maskBlurText"></span>
<br />
@ -104,7 +114,7 @@
<button onclick="downloadCanvas()">Save canvas</button>
<br />
<label for="upscalers">Choose upscaler</label>
<select id="upscalers"></select>
<select id="upscalers" class="wideSelect"></select>
<button onclick="upscaleAndDownload()">
Upscale (might take a sec)
</button>
@ -136,6 +146,10 @@
<label for="heldButton">Mouse button:</label>
<span id="heldButton"></span>
<br />
<button id="resetToDefaults" onclick="resetToDefaults()">
Reset to defaults
</button>
<br />
<span id="version">
<a href="https://github.com/zero01101/openOutpaint" target="_blank">
Alpha release v0.0.7.5
@ -200,9 +214,8 @@
<script src="js/lib/layers.js" type="text/javascript"></script>
<script src="js/lib/commands.js" type="text/javascript"></script>
<script src="js/settingsbar.js" type="text/javascript"></script>
<script src="js/lib/toolbar.js" type="text/javascript"></script>
<script src="js/lib/ui.js" type="text/javascript"></script>
<script
src="js/initalize/layers.populate.js"
@ -210,21 +223,25 @@
<!-- Content -->
<script src="js/index.js" type="text/javascript"></script>
<script src="js/shortcuts.js" type="text/javascript"></script>
<script src="js/ui/floating/history.js" type="text/javascript"></script>
<!-- Load Tools -->
<script src="js/ui/tool/dream.js" type="text/javascript"></script>
<script src="js/ui/tool/maskbrush.js" type="text/javascript"></script>
<script src="js/ui/tool/colorbrush.js" type="text/javascript"></script>
<script src="js/ui/tool/select.js" type="text/javascript"></script>
<script src="js/ui/tool/stamp.js" type="text/javascript"></script>
<!-- Initialize -->
<script
src="js/initalize/shortcuts.populate.js"
type="text/javascript"></script>
<script
src="js/initalize/toolbar.populate.js"
type="text/javascript"></script>
<script
src="js/initalize/debug.populate.js"
type="text/javascript"></script>
<script src="js/initalize/ui.populate.js" type="text/javascript"></script>
</body>
</html>

View file

@ -24,6 +24,7 @@ var stableDiffusionData = {
enable_hr: false,
firstphase_width: 0,
firstphase_height: 0,
styles: [],
// here's some more fields that might be useful
// ---txt2img specific:
@ -48,6 +49,7 @@ var stableDiffusionData = {
};
// stuff things use
let debug = false;
var returnedImages;
var imageIndex = 0;
var tmpImgXYWH = {};
@ -58,7 +60,6 @@ var frameX = 512;
var frameY = 512;
var drawThis = {};
const basePixelCount = 64; //64 px - ALWAYS 64 PX
var scaleFactor = 8; //x64 px
var snapToGrid = true;
var backupMaskPaintCanvas; //???
var backupMaskPaintCtx; //...? look i am bad at this
@ -115,7 +116,6 @@ function startup() {
changeSeed();
changeOverMaskPx();
changeHiResFix();
document.getElementById("scaleFactor").value = scaleFactor;
}
/**
@ -126,6 +126,7 @@ function testHostConfiguration() {
* Check host configuration
*/
const hostEl = document.getElementById("host");
hostEl.value = localStorage.getItem("host");
const requestHost = (prompt, def = "http://127.0.0.1:7860") => {
let value = window.prompt(prompt, def);
@ -168,6 +169,10 @@ async function testHostConnection() {
let firstTimeOnline = true;
const setConnectionStatus = (status) => {
const connectionIndicatorText = document.getElementById(
"connection-status-indicator-text"
);
const statuses = {
online: () => {
connectionIndicator.classList.add("online");
@ -177,7 +182,8 @@ async function testHostConnection() {
"before",
"server-error"
);
connectionIndicator.title = "Connected";
connectionIndicatorText.textContent = connectionIndicator.title =
"Connected";
connectionStatus = true;
},
error: () => {
@ -188,6 +194,7 @@ async function testHostConnection() {
"before",
"cors-issue"
);
connectionIndicatorText.textContent = "Error";
connectionIndicator.title =
"Server is online, but is returning an error response";
connectionStatus = false;
@ -200,6 +207,7 @@ async function testHostConnection() {
"before",
"server-error"
);
connectionIndicatorText.textContent = "CORS";
connectionIndicator.title =
"Server is online, but CORS is blocking our requests";
connectionStatus = false;
@ -212,6 +220,7 @@ async function testHostConnection() {
"before",
"server-error"
);
connectionIndicatorText.textContent = "Offline";
connectionIndicator.title =
"Server seems to be offline. Please check the console for more information.";
connectionStatus = false;
@ -224,6 +233,7 @@ async function testHostConnection() {
"offline",
"server-error"
);
connectionIndicatorText.textContent = "Waiting";
connectionIndicator.title = "Waiting for check to complete.";
connectionStatus = false;
},
@ -254,6 +264,7 @@ async function testHostConnection() {
setConnectionStatus("online");
// Load data as soon as connection is first stablished
if (firstTimeOnline) {
getStyles();
getSamplers();
getUpscalers();
getModels();
@ -272,10 +283,7 @@ async function testHostConnection() {
await fetch(url, {mode: "no-cors"});
setConnectionStatus("corsissue");
const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${document.URL.substring(
0,
document.URL.length - 1
)}'`;
const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${window.location.protocol}//${window.location.host}/'`;
console.error(message);
if (notify) alert(message);
} catch (e) {
@ -484,24 +492,46 @@ const makeSlider = (
max,
step,
defaultValue,
textStep = null,
valuecb = null
) => {
const local = localStorage.getItem(lsKey);
const local = lsKey && localStorage.getItem(lsKey);
const def = parseFloat(local === null ? defaultValue : local);
let cb = (v) => {
stableDiffusionData[lsKey] = v;
if (lsKey) localStorage.setItem(lsKey, v);
};
if (valuecb) {
cb = (v) => {
valuecb(v);
localStorage.setItem(lsKey, v);
};
}
return createSlider(label, el, {
valuecb:
valuecb ||
((v) => {
stableDiffusionData[lsKey] = v;
localStorage.setItem(lsKey, v);
}),
valuecb: cb,
min,
max,
step,
defaultValue: def,
textStep,
});
};
makeSlider(
"Resolution",
document.getElementById("resolution"),
"resolution",
64,
1024,
64,
512,
2,
(v) => {
stableDiffusionData.width = stableDiffusionData.height = v;
stableDiffusionData.firstphase_width =
stableDiffusionData.firstphase_height = v / 2;
}
);
makeSlider(
"CFG Scale",
document.getElementById("cfgScale"),
@ -509,7 +539,8 @@ makeSlider(
-1,
25,
0.5,
7.0
7.0,
0.1
);
makeSlider(
"Batch Size",
@ -529,27 +560,17 @@ makeSlider(
1,
2
);
makeSlider(
"Scale Factor",
document.getElementById("scaleFactor"),
"scale_factor",
1,
16,
1,
8,
(v) => {
scaleFactor = v;
}
);
makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 1, 30);
makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1);
function changeSnapMode() {
snapToGrid = document.getElementById("cbxSnap").checked;
}
function changeMaskBlur() {
stableDiffusionData.mask_blur = document.getElementById("maskBlur").value;
stableDiffusionData.mask_blur = parseInt(
document.getElementById("maskBlur").value
);
localStorage.setItem("mask_blur", stableDiffusionData.mask_blur);
}
@ -742,6 +763,78 @@ function changeModel() {
});
}
async function getStyles() {
/** @type {HTMLSelectElement} */
var styleSelect = document.getElementById("styleSelect");
var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles";
try {
const response = await fetch(url);
/** @type {{name: string, prompt: string, negative_prompt: string}[]} */
const data = await response.json();
/** @type {string[]} */
let stored = null;
try {
stored = JSON.parse(localStorage.getItem("promptStyle"));
// doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist?
if (stored == null) stored = [];
} catch (e) {
stored = [];
}
data.forEach((style) => {
const option = document.createElement("option");
option.classList.add("style-select-option");
option.text = style.name;
option.value = style.name;
option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`;
if (stored.length === 0) option.selected = style.name === "None";
else
option.selected = !!stored.find(
(styleName) => style.name === styleName
);
styleSelect.add(option);
});
changeStyles();
stored.forEach((styleName, index) => {
if (!data.findIndex((style) => style.name === styleName)) {
stored.splice(index, 1);
}
});
localStorage.setItem("promptStyle", JSON.stringify(stored));
} catch (e) {
console.warn("[index] Failed to fetch prompt styles");
console.warn(e);
}
}
function changeStyles() {
/** @type {HTMLSelectElement} */
const styleSelectEl = document.getElementById("styleSelect");
const selected = Array.from(styleSelectEl.options).filter(
(option) => option.selected
);
let selectedString = selected.map((option) => option.value);
if (selectedString.find((selected) => selected === "None")) {
selectedString = [];
Array.from(styleSelectEl.options).forEach((option) => {
if (option.value !== "None") option.selected = false;
});
}
localStorage.setItem("promptStyle", JSON.stringify(selectedString));
// change the model
if (selectedString.length > 0)
console.log(`[index] Changing styles to ${selectedString.join(", ")}`);
else console.log(`[index] Clearing styles`);
stableDiffusionData.styles = selectedString;
}
function getSamplers() {
var samplerSelect = document.getElementById("samplerSelect");
var url = document.getElementById("host").value + "/sdapi/v1/samplers";
@ -783,7 +876,7 @@ async function upscaleAndDownload() {
var upscaler = document.getElementById("upscalers").value;
var url =
document.getElementById("host").value + "/sdapi/v1/extra-single-image/";
var imgdata = croppedCanvas.toDataURL("image/png");
var imgdata = croppedCanvas.canvas.toDataURL("image/png");
var data = {
"resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options
upscaling_resize: upscale_factor,
@ -880,3 +973,9 @@ imageCollection.element.addEventListener(
},
{passive: false}
);
function resetToDefaults() {
if (confirm("Are you sure you want to clear your settings?")) {
localStorage.clear();
}
}

View file

@ -24,6 +24,11 @@ mouse.listen.world.onmousemove.on((evn) => {
*/
const toggledebug = () => {
const hidden = debugCanvas.style.display === "none";
if (hidden) debugLayer.unhide();
else debugLayer.hide();
if (hidden) {
debugLayer.unhide();
debug = true;
} else {
debugLayer.hide();
debug = false;
}
};

View file

@ -54,7 +54,7 @@ mouse.registerContext(
const target = evn.target;
// Get element bounding rect
const bb = target.getBoundingClientRect();
const bb = imageCollection.element.getBoundingClientRect();
// Get element width/height (css, cause I don't trust client sizes in chrome anymore)
const w = imageCollection.size.w;
@ -148,11 +148,13 @@ mouse.listen.window.onwheel.on((evn) => {
viewport.transform(imageCollection.element);
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
if (debug) {
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
}
}
});
@ -173,11 +175,13 @@ mouse.listen.window.btn.middle.onpaint.on((evn) => {
}
viewport.transform(imageCollection.element);
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
if (debug) {
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
}
});
mouse.listen.window.btn.middle.onpaintend.on((evn) => {

View file

@ -14,6 +14,9 @@ keyboard.onShortcut({key: "KeyD"}, () => {
keyboard.onShortcut({key: "KeyM"}, () => {
tools.maskbrush.enable();
});
keyboard.onShortcut({key: "KeyC"}, () => {
tools.colorbrush.enable();
});
keyboard.onShortcut({key: "KeyI"}, () => {
tools.img2img.enable();
});

View file

@ -15,6 +15,7 @@ toolbar.addSeparator();
* Mask Brush tool
*/
tools.maskbrush = maskBrushTool();
tools.colorbrush = colorBrushTool();
/**
* Image Editing tools

View file

@ -0,0 +1,35 @@
document.querySelectorAll(".floating-window").forEach((w) => {
makeDraggable(w);
});
var coll = document.getElementsByClassName("collapsible");
for (var i = 0; i < coll.length; i++) {
let active = false;
coll[i].addEventListener("click", function () {
var content = this.nextElementSibling;
if (!active) {
this.classList.add("active");
content.classList.add("active");
} else {
this.classList.remove("active");
content.classList.remove("active");
}
const observer = new ResizeObserver(() => {
if (active) content.style.maxHeight = content.scrollHeight + "px";
});
Array.from(content.children).forEach((child) => {
observer.observe(child);
});
if (active) {
content.style.maxHeight = null;
active = false;
} else {
content.style.maxHeight = content.scrollHeight + "px";
active = true;
}
});
}

View file

@ -278,7 +278,30 @@ commands.createCommand(
}
// Apply command
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
const style = state.context.fillStyle;
state.context.fillStyle = "black";
const op = state.context.globalCompositeOperation;
state.context.globalCompositeOperation = "destination-out";
if (options.mask)
state.context.drawImage(
options.mask,
state.box.x,
state.box.y,
state.box.w,
state.box.h
);
else
state.context.fillRect(
state.box.x,
state.box.y,
state.box.w,
state.box.h
);
state.context.fillStyle = style;
state.context.globalCompositeOperation = op;
},
(title, state) => {
// Clear destination area

View file

@ -288,7 +288,7 @@ window.addEventListener(
window.addEventListener(
"mousemove",
(evn) => {
mouse._contexts.forEach((context) => {
mouse._contexts.forEach(async (context) => {
const target = context.target;
const name = context.name;

View file

@ -172,13 +172,30 @@ const layers = {
* Moves this layer to another location
*
* @param {number} x X coordinate of the top left of the canvas
* @param {number} y X coordinate of the top left of the canvas
* @param {number} y Y coordinate of the top left of the canvas
*/
moveTo(x, y) {
canvas.style.left = `${x}px`;
canvas.style.top = `${y}px`;
},
/**
* Resizes layer in place
*
* @param {number} w New width
* @param {number} h New height
*/
resize(w, h) {
canvas.width = Math.round(
options.resolution.w * (w / options.bb.w)
);
canvas.height = Math.round(
options.resolution.h * (h / options.bb.h)
);
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
},
// Hides this layer (don't draw)
hide() {
this.canvas.style.display = "none";

View file

@ -150,17 +150,20 @@ const _toolbar_input = {
return {checkbox, label};
},
slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => {
slider: (state, dataKey, text, options = {}) => {
defaultOpt(options, {min: 0, max: 1, step: 0.1, textStep: null, cb: null});
const slider = document.createElement("div");
const value = createSlider(text, slider, {
min,
max,
step,
min: options.min,
max: options.max,
step: options.step,
valuecb: (v) => {
state[dataKey] = v;
options.cb && options.cb(v);
},
defaultValue: state[dataKey],
textStep: options.textStep,
});
return {
@ -172,21 +175,3 @@ const _toolbar_input = {
};
},
};
/**
* Dream and img2img tools
*/
const _reticle_draw = (evn, snapToGrid = true) => {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
snapToGrid && basePixelCount
);
// draw targeting square reticle thingy cursor
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
};

View file

@ -1,9 +1,18 @@
/**
* This is a function that makes an HTMLElement draggable.
*
* The element must contain at least one child element with the class
* 'draggable', which will make it the handle for dragging the element
*
* @param {HTMLElement} element Element to make Draggable
*/
function makeDraggable(element) {
let dragging = false;
let offset = {x: 0, y: 0};
const margin = 10;
// Keeps the draggable element inside the window
const fixPos = () => {
const dbb = element.getBoundingClientRect();
if (dbb.left < margin) element.style.left = margin + "px";
@ -17,6 +26,7 @@ function makeDraggable(element) {
dbb.top + (window.innerHeight - margin - dbb.bottom) + "px";
};
// Detects the start of the mouse dragging event
mouse.listen.window.btn.left.onpaintstart.on((evn) => {
if (
element.contains(evn.target) &&
@ -29,6 +39,7 @@ function makeDraggable(element) {
}
});
// Runs when mouse moves
mouse.listen.window.btn.left.onpaint.on((evn) => {
if (dragging) {
element.style.right = null;
@ -40,53 +51,30 @@ function makeDraggable(element) {
}
});
// Stops dragging the element
mouse.listen.window.btn.left.onpaintend.on((evn) => {
dragging = false;
});
// Redraw after window resize
window.addEventListener("resize", () => {
fixPos();
});
}
document.querySelectorAll(".floating-window").forEach((w) => {
makeDraggable(w);
});
var coll = document.getElementsByClassName("collapsible");
for (var i = 0; i < coll.length; i++) {
let active = false;
coll[i].addEventListener("click", function () {
var content = this.nextElementSibling;
if (!active) {
this.classList.add("active");
content.classList.add("active");
} else {
this.classList.remove("active");
content.classList.remove("active");
}
const observer = new ResizeObserver(() => {
if (active) content.style.maxHeight = content.scrollHeight + "px";
});
Array.from(content.children).forEach((child) => {
observer.observe(child);
});
if (active) {
content.style.maxHeight = null;
active = false;
} else {
content.style.maxHeight = content.scrollHeight + "px";
active = true;
}
});
}
/**
* Slider Inputs
* Creates a custom slider element from a given div element
*
* @param {string} name The display name of the sliders
* @param {HTMLElement} wrapper The element to transform into a slider
* @param {object} options Extra options
* @param {number} options.min The minimum value of the slider
* @param {number} options.max The maximum value of the slider
* @param {number} options.step The step size for the slider
* @param {number} option.defaultValue The default value of the slider
* @param {number} [options.textStep=step] The step size for the slider text and setvalue \
* (usually finer, and an integer divisor of step size)
* @returns {{value: number}} A reference to the value of the slider
*/
function createSlider(name, wrapper, options = {}) {
defaultOpt(options, {
@ -95,6 +83,7 @@ function createSlider(name, wrapper, options = {}) {
max: 1,
step: 0.1,
defaultValue: 0.7,
textStep: null,
});
let value = options.defaultValue;
@ -106,6 +95,15 @@ function createSlider(name, wrapper, options = {}) {
phantomRange.max = options.max;
phantomRange.step = options.step;
let phantomTextRange = phantomRange;
if (options.textStep) {
phantomTextRange = document.createElement("input");
phantomTextRange.type = "range";
phantomTextRange.min = options.min;
phantomTextRange.max = options.max;
phantomTextRange.step = options.textStep;
}
// Build slider element
const underEl = document.createElement("div");
underEl.classList.add("under");
@ -128,8 +126,8 @@ function createSlider(name, wrapper, options = {}) {
// Set value
const setValue = (val) => {
phantomRange.value = val;
value = parseFloat(phantomRange.value);
phantomTextRange.value = val;
value = parseFloat(phantomTextRange.value);
bar.style.width = `${
100 * ((value - options.min) / (options.max - options.min))
}%`;
@ -170,17 +168,15 @@ function createSlider(name, wrapper, options = {}) {
mouse.listen.window.btn.left.ondrag.on((evn) => {
if (evn.initialTarget === overEl) {
setValue(
Math.max(
options.min,
Math.min(
options.max,
(evn.evn.layerX / wrapper.offsetWidth) *
(options.max - options.min) +
options.min
)
phantomRange.value = Math.max(
options.min,
Math.min(
options.max,
(evn.evn.layerX / wrapper.offsetWidth) * (options.max - options.min) +
options.min
)
);
setValue(parseFloat(phantomRange.value));
}
});

View file

@ -145,21 +145,14 @@ function makeWriteOnce(obj, name = "write-once object", exceptions = []) {
* Snaps a single value to an infinite grid
*
* @param {number} i Original value to be snapped
* @param {boolean} scaled If grid will change alignment for odd scaleFactor values (default: true)
* @param {number} gridSize Size of the grid
* @param {number} [offset=0] Value to offset the grid. Should be in the rande [0, gridSize[
* @param {number} [gridSize=64] Size of the grid
* @returns an offset, in which [i + offset = (a location snapped to the grid)]
*/
function snap(i, scaled = true, gridSize = 64) {
// very cheap test proof of concept but it works surprisingly well
var scaleOffset = 0;
if (scaled) {
if (scaleFactor % 2 != 0) {
// odd number, snaps to center of cell, oops
scaleOffset = gridSize / 2;
}
}
const modulus = i % gridSize;
var snapOffset = modulus - scaleOffset;
function snap(i, offset = 0, gridSize = 64) {
const modulus = (i - offset) % gridSize;
var snapOffset = modulus;
if (modulus > gridSize / 2) snapOffset = modulus - gridSize;
if (snapOffset == 0) {
@ -175,19 +168,20 @@ function snap(i, scaled = true, gridSize = 64) {
* @param {number} cy - y-coordinate of the center of the box
* @param {number} w - the width of the box
* @param {height} h - the height of the box
* @param {number | null} gridSnap - The size of the grid to snap to
* @param {?number} gridSnap - The size of the grid to snap to
* @param {number} [offset=0] - How much to offset the grid by
* @returns {BoundingBox} - A bounding box object centered at (cx, cy)
*/
function getBoundingBox(cx, cy, w, h, gridSnap = null) {
const offset = {x: 0, y: 0};
function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) {
const offs = {x: 0, y: 0};
const box = {x: 0, y: 0};
if (gridSnap) {
offset.x = snap(cx, true, gridSnap);
offset.y = snap(cy, true, gridSnap);
offs.x = snap(cx, offset, gridSnap);
offs.y = snap(cy, offset, gridSnap);
}
box.x = offset.x + cx;
box.y = offset.y + cy;
box.x = offs.x + cx;
box.y = offs.y + cy;
return {
x: Math.floor(box.x - w / 2),
@ -197,57 +191,58 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null) {
};
}
class NoContentError extends Error {}
/**
* Crops a given canvas to content, returning a new canvas object with the content in it.
*
* @param {HTMLCanvasElement} sourceCanvas Canvas to get a content crop from
* @returns {HTMLCanvasElement} A new canvas with the cropped part of the image
* @param {object} options Extra options
* @param {number} [options.border=0] Extra border around the content
* @returns {{canvas: HTMLCanvasElement, bb: BoundingBox}} A new canvas with the cropped part of the image
*/
function cropCanvas(sourceCanvas) {
var w = sourceCanvas.width;
var h = sourceCanvas.height;
var pix = {x: [], y: []};
var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h);
var x, y, index;
function cropCanvas(sourceCanvas, options = {}) {
defaultOpt(options, {border: 0});
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
const w = sourceCanvas.width;
const h = sourceCanvas.height;
var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h);
/** @type {BoundingBox} */
const bb = {x: 0, y: 0, w: 0, h: 0};
let minx = w;
let maxx = -1;
let miny = h;
let maxy = -1;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
// lol i need to learn what this part does
index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords
const index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords
//this part i get, this is checking that 4th RGBA byte for opacity
if (imageData.data[index + 3] > 0) {
pix.x.push(x);
pix.y.push(y);
minx = Math.min(minx, x);
maxx = Math.max(maxx, x);
miny = Math.min(miny, y);
maxy = Math.max(maxy, y);
}
}
}
// ...need to learn what this part does too :badpokerface:
// is this just determining the boundaries of non-transparent pixel data?
pix.x.sort(function (a, b) {
return a - b;
});
pix.y.sort(function (a, b) {
return a - b;
});
var n = pix.x.length - 1;
w = pix.x[n] - pix.x[0] + 1;
h = pix.y[n] - pix.y[0] + 1;
// yup sure looks like it
try {
var cut = sourceCanvas
.getContext("2d")
.getImageData(pix.x[0], pix.y[0], w, h);
var cutCanvas = document.createElement("canvas");
cutCanvas.width = w;
cutCanvas.height = h;
cutCanvas.getContext("2d").putImageData(cut, 0, 0);
} catch (ex) {
// probably empty image
//TODO confirm edge cases?
cutCanvas = null;
}
return cutCanvas;
bb.x = minx - options.border;
bb.y = miny - options.border;
bb.w = maxx - minx + 2 * options.border;
bb.h = maxy - miny + 2 * options.border;
if (maxx < 0) throw new NoContentError("Canvas has no content to crop");
var cutCanvas = document.createElement("canvas");
cutCanvas.width = bb.w;
cutCanvas.height = bb.h;
cutCanvas
.getContext("2d")
.drawImage(sourceCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
return {canvas: cutCanvas, bb};
}
/**
@ -276,7 +271,7 @@ function downloadCanvas(options = {}) {
if (options.filename) link.download = options.filename;
var croppedCanvas = options.cropToContent
? cropCanvas(options.canvas)
? cropCanvas(options.canvas).canvas
: options.canvas;
if (croppedCanvas != null) {
link.href = croppedCanvas.toDataURL("image/png");

252
js/ui/tool/colorbrush.js Normal file
View file

@ -0,0 +1,252 @@
const _color_brush_draw_callback = (evn, state) => {
const ctx = state.drawLayer.ctx;
ctx.strokeStyle = state.color;
ctx.filter = "blur(" + state.brushBlur + "px)";
ctx.lineWidth = state.brushSize;
ctx.beginPath();
ctx.moveTo(
evn.px === undefined ? evn.x : evn.px,
evn.py === undefined ? evn.y : evn.py
);
ctx.lineTo(evn.x, evn.y);
ctx.lineJoin = ctx.lineCap = "round";
ctx.stroke();
};
const _color_brush_erase_callback = (evn, state, ctx) => {
ctx.strokeStyle = "black";
ctx.lineWidth = state.brushSize;
ctx.beginPath();
ctx.moveTo(
evn.px === undefined ? evn.x : evn.px,
evn.py === undefined ? evn.y : evn.py
);
ctx.lineTo(evn.x, evn.y);
ctx.lineJoin = ctx.lineCap = "round";
ctx.stroke();
};
const colorBrushTool = () =>
toolbar.registerTool(
"res/icons/brush.svg",
"Color Brush",
(state, opt) => {
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.world.pos});
state.drawLayer = imageCollection.registerLayer(null, {
after: imgLayer,
});
state.eraseLayer = imageCollection.registerLayer(null, {
after: imgLayer,
});
state.eraseLayer.canvas.style.display = "none";
state.eraseBackup = imageCollection.registerLayer(null, {
after: imgLayer,
});
state.eraseBackup.canvas.style.display = "none";
// Start Listeners
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.onwheel.on(state.wheelcb);
mouse.listen.world.btn.left.onpaintstart.on(state.drawstartcb);
mouse.listen.world.btn.left.onpaint.on(state.drawcb);
mouse.listen.world.btn.left.onpaintend.on(state.drawendcb);
mouse.listen.world.btn.right.onpaintstart.on(state.erasestartcb);
mouse.listen.world.btn.right.onpaint.on(state.erasecb);
mouse.listen.world.btn.right.onpaintend.on(state.eraseendcb);
// Display Color
setMask("none");
},
(state, opt) => {
// Clear Listeners
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.onwheel.clear(state.wheelcb);
mouse.listen.world.btn.left.onpaintstart.clear(state.drawstartcb);
mouse.listen.world.btn.left.onpaint.clear(state.drawcb);
mouse.listen.world.btn.left.onpaintend.clear(state.drawendcb);
mouse.listen.world.btn.right.onpaintstart.clear(state.erasestartcb);
mouse.listen.world.btn.right.onpaint.clear(state.erasecb);
mouse.listen.world.btn.right.onpaintend.clear(state.eraseendcb);
// Delete layer
imageCollection.deleteLayer(state.drawLayer);
imageCollection.deleteLayer(state.eraseBackup);
imageCollection.deleteLayer(state.eraseLayer);
},
{
init: (state) => {
state.config = {
brushScrollSpeed: 1 / 5,
minBrushSize: 2,
maxBrushSize: 500,
minBlur: 0,
maxBlur: 30,
};
state.color = "#FFFFFF";
state.brushSize = 32;
state.brushBlur = 0;
state.affectMask = true;
state.setBrushSize = (size) => {
state.brushSize = size;
state.ctxmenu.brushSizeRange.value = size;
state.ctxmenu.brushSizeText.value = size;
};
state.movecb = (evn) => {
// draw big translucent white blob cursor
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
ovCtx.beginPath();
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
ovCtx.fillStyle = state.color + "50";
ovCtx.fill();
};
state.wheelcb = (evn) => {
if (!evn.evn.ctrlKey) {
state.brushSize = state.setBrushSize(
state.brushSize -
Math.floor(state.config.brushScrollSpeed * evn.delta)
);
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(evn);
}
};
state.drawstartcb = (evn) => {
if (state.affectMask) _mask_brush_draw_callback(evn, state);
_color_brush_draw_callback(evn, state);
};
state.drawcb = (evn) => {
if (state.affectMask) _mask_brush_draw_callback(evn, state);
_color_brush_draw_callback(evn, state);
};
state.drawendcb = (evn) => {
const canvas = state.drawLayer.canvas;
const ctx = state.drawLayer.ctx;
const cropped = cropCanvas(canvas, {border: 10});
const bb = cropped.bb;
commands.runCommand("drawImage", "Color Brush Draw", {
image: cropped.canvas,
...bb,
});
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
};
state.erasestartcb = (evn) => {
if (state.affectMask) _mask_brush_erase_callback(evn, state);
// Make a backup of the current image to apply erase later
const bkpcanvas = state.eraseBackup.canvas;
const bkpctx = state.eraseBackup.ctx;
bkpctx.clearRect(0, 0, bkpcanvas.width, bkpcanvas.height);
bkpctx.drawImage(imgCanvas, 0, 0);
imgCtx.globalCompositeOperation = "destination-out";
_color_brush_erase_callback(evn, state, imgCtx);
imgCtx.globalCompositeOperation = "source-over";
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
};
state.erasecb = (evn) => {
if (state.affectMask) _mask_brush_erase_callback(evn, state);
imgCtx.globalCompositeOperation = "destination-out";
_color_brush_erase_callback(evn, state, imgCtx);
imgCtx.globalCompositeOperation = "source-over";
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
};
state.eraseendcb = (evn) => {
const canvas = state.eraseLayer.canvas;
const ctx = state.eraseLayer.ctx;
const bkpcanvas = state.eraseBackup.canvas;
const cropped = cropCanvas(canvas, {border: 10});
const bb = cropped.bb;
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
imgCtx.drawImage(bkpcanvas, 0, 0);
commands.runCommand("eraseImage", "Color Brush Erase", {
mask: cropped.canvas,
...bb,
});
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
};
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Affects Mask Checkbox
const affectMaskCheckbox = _toolbar_input.checkbox(
state,
"affectMask",
"Affect Mask"
).label;
state.ctxmenu.affectMaskCheckbox = affectMaskCheckbox;
// Brush size slider
const brushSizeSlider = _toolbar_input.slider(
state,
"brushSize",
"Brush Size",
{
min: state.config.minBrushSize,
max: state.config.maxBrushSize,
step: 5,
textStep: 1,
}
);
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
state.setBrushSize = brushSizeSlider.setValue;
// Brush size slider
const brushBlurSlider = _toolbar_input.slider(
state,
"brushBlur",
"Brush Blur",
{
min: state.config.minBlur,
max: state.config.maxBlur,
step: 1,
}
);
state.ctxmenu.brushBlurSlider = brushBlurSlider.slider;
// Brush color
const brushColorPicker = document.createElement("input");
brushColorPicker.type = "color";
brushColorPicker.style.width = "100%";
brushColorPicker.value = state.color;
brushColorPicker.addEventListener("input", (evn) => {
state.color = evn.target.value;
});
state.ctxmenu.brushColorPicker = brushColorPicker;
}
menu.appendChild(state.ctxmenu.affectMaskCheckbox);
menu.appendChild(state.ctxmenu.brushSizeSlider);
menu.appendChild(state.ctxmenu.brushBlurSlider);
menu.appendChild(state.ctxmenu.brushColorPicker);
},
shortcut: "C",
}
);

View file

@ -125,16 +125,40 @@ const _generate = async (endpoint, request, bb) => {
image.src = "data:image/png;base64," + images[at];
image.addEventListener("load", () => {
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
if (images[at]) layer.ctx.drawImage(image, bb.x, bb.y);
if (images[at])
layer.ctx.drawImage(
image,
0,
0,
image.width,
image.height,
bb.x,
bb.y,
bb.w,
bb.h
);
});
};
const stopMarchingAnts = march(bb);
// First Dream Run
let stopProgress = _monitorProgress(bb);
images.push(...(await _dream(endpoint, requestCopy)));
stopProgress();
console.info(`[dream] Generating images for prompt '${request.prompt}'`);
console.debug(request);
let stopProgress = null;
try {
stopProgress = _monitorProgress(bb);
images.push(...(await _dream(endpoint, requestCopy)));
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
);
console.warn(`[dream] Error generating images:`);
console.warn(e);
} finally {
stopProgress();
}
// Image navigation
const prevImg = () => {
@ -161,6 +185,8 @@ const _generate = async (endpoint, request, bb) => {
commands.runCommand("drawImage", "Image Dream", {
x: bb.x,
y: bb.y,
w: bb.w,
h: bb.h,
image: img,
});
clean(true);
@ -168,11 +194,19 @@ const _generate = async (endpoint, request, bb) => {
};
const makeMore = async () => {
let stopProgress = _monitorProgress(bb);
images.push(...(await _dream(endpoint, requestCopy)));
stopProgress();
imageindextxt.textContent = `${at + 1}/${images.length}`;
try {
stopProgress = _monitorProgress(bb);
images.push(...(await _dream(endpoint, requestCopy)));
imageindextxt.textContent = `${at + 1}/${images.length}`;
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
);
console.warn(`[dream] Error generating images:`);
console.warn(e);
} finally {
stopProgress();
}
};
const discardImg = async () => {
@ -316,8 +350,8 @@ const dream_generate_callback = async (evn, state) => {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
@ -332,13 +366,6 @@ const dream_generate_callback = async (evn, state) => {
// Don't allow another image until is finished
blockNewImages = true;
// Setup some basic information for SD
request.width = bb.w;
request.height = bb.h;
request.firstphase_width = bb.w / 2;
request.firstphase_height = bb.h / 2;
// Use txt2img if canvas is blank
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) {
// Dream
@ -355,13 +382,23 @@ const dream_generate_callback = async (evn, state) => {
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
imgCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = "#000F";
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
if (state.invertMask) {
// overmasking by definition is entirely pointless with an inverted mask outpaint
// since it should explicitly avoid brushed masks too, we just won't even bother
@ -374,22 +411,42 @@ const dream_generate_callback = async (evn, state) => {
bb.h,
0,
0,
bb.w,
bb.h
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
auxCtx.drawImage(
imgCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
} else {
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
auxCtx.drawImage(
imgCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
// here's where to overmask to avoid including the brushed mask
// 99% of my issues were from failing to set source-over for the overmask blotches
if (state.overMaskPx > 0) {
// transparent to white first
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
applyOvermask(auxCanvas, auxCtx, state.overMaskPx);
}
@ -402,13 +459,13 @@ const dream_generate_callback = async (evn, state) => {
bb.h,
0,
0,
bb.w,
bb.h
request.width,
request.height
);
}
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
request.mask = auxCanvas.toDataURL();
// Dream
_generate("img2img", request, bb);
@ -419,8 +476,8 @@ const dream_erase_callback = (evn, state) => {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
commands.runCommand("eraseImage", "Erase Area", bb);
@ -436,14 +493,25 @@ function applyOvermask(canvas, ctx, px) {
if (ctxImgData.data[i] == 255) {
// white pixel?
// just blotch all over the thing
var rando = Math.floor(Math.random() * px);
/**
* This should probably have a better randomness profile for the overmasking
*
* Essentially, we want to have much more smaller values for randomness than big ones,
* because big values overshadow smaller circles and kinda ignores their randomness.
*
* And also, we want the profile to become more extreme the bigger the overmask size,
* because bigger px values also make bigger circles ocuppy more horizontal space.
*/
let lowRandom =
Math.atan(Math.random() * 10 - 10) / Math.abs(Math.atan(-10)) + 1;
lowRandom = Math.pow(lowRandom, px / 8);
var rando = Math.floor(lowRandom * px);
ctx.beginPath();
ctx.arc(
(i / 4) % canvas.width,
Math.floor(i / 4 / canvas.width),
scaleFactor +
rando +
(rando > scaleFactor ? rando / scaleFactor : scaleFactor / rando), // was 4 * sf + rando, too big, but i think i want it more ... random
rando, // was 4 * sf + rando, too big, but i think i want it more ... random
0,
2 * Math.PI,
true
@ -462,8 +530,8 @@ const dream_img2img_callback = (evn, state) => {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
@ -484,13 +552,6 @@ const dream_img2img_callback = (evn, state) => {
// Don't allow another image until is finished
blockNewImages = true;
// Setup some basic information for SD
request.width = bb.w;
request.height = bb.h;
request.firstphase_width = bb.w / 2;
request.firstphase_height = bb.h / 2;
// Use img2img
// Temporary canvas for init image and mask generation
@ -502,36 +563,56 @@ const dream_img2img_callback = (evn, state) => {
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
imgCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F";
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.globalCompositeOperation = "destination-out";
auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF";
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.fillRect(0, 0, request.width, request.height);
// Border Mask
if (state.keepBorderSize > 0) {
auxCtx.globalCompositeOperation = "source-over";
auxCtx.fillStyle = "#000F";
auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h);
auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize);
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
auxCtx.fillRect(
bb.w - state.keepBorderSize,
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
bb.h
request.height
);
auxCtx.fillRect(
0,
bb.h - state.keepBorderSize,
bb.w,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
}
@ -544,6 +625,42 @@ const dream_img2img_callback = (evn, state) => {
}
};
/**
* Dream and img2img tools
*/
const _reticle_draw = (evn, state) => {
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// draw targeting square reticle thingy cursor
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
return () => {
ovCtx.clearRect(bb.x - 10, bb.y - 10, bb.w + 20, bb.h + 20);
};
};
/**
* Generic wheel handler
*/
const _dream_onwheel = (evn, state) => {
if (!evn.evn.ctrlKey) {
const v =
state.cursorSize -
Math.floor(state.config.cursorSizeScrollSpeed * evn.delta);
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
state.mousemovecb(evn);
}
};
/**
* Registers Tools
*/
@ -560,6 +677,7 @@ const dreamTool = () =>
// Start Listeners
mouse.listen.world.onmousemove.on(state.mousemovecb);
mouse.listen.world.onwheel.on(state.wheelcb);
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
mouse.listen.world.btn.right.onclick.on(state.erasecb);
@ -569,6 +687,7 @@ const dreamTool = () =>
(state, opt) => {
// Clear Listeners
mouse.listen.world.onmousemove.clear(state.mousemovecb);
mouse.listen.world.onwheel.clear(state.wheelcb);
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
@ -577,12 +696,25 @@ const dreamTool = () =>
},
{
init: (state) => {
state.config = {
cursorSizeScrollSpeed: 1,
};
state.cursorSize = 512;
state.snapToGrid = true;
state.invertMask = false;
state.overMaskPx = 0;
state.mousemovecb = (evn) => {
state.erasePrevReticle = () =>
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
_reticle_draw(evn, state.snapToGrid);
state.mousemovecb = (evn) => {
state.erasePrevReticle();
state.erasePrevReticle = _reticle_draw(evn, state);
};
state.wheelcb = (evn) => {
_dream_onwheel(evn, state);
};
state.dreamcb = (evn) => {
dream_generate_callback(evn, state);
@ -593,6 +725,22 @@ const dreamTool = () =>
if (!state.ctxmenu) {
state.ctxmenu = {};
// Cursor Size Slider
const cursorSizeSlider = _toolbar_input.slider(
state,
"cursorSize",
"Cursor Size",
{
min: 0,
max: 2048,
step: 128,
textStep: 2,
}
);
state.setCursorSize = cursorSizeSlider.setValue;
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
// Snap to Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
@ -615,12 +763,16 @@ const dreamTool = () =>
state,
"overMaskPx",
"Overmask px",
0,
128,
1
{
min: 0,
max: 64,
step: 5,
textStep: 1,
}
).slider;
}
menu.appendChild(state.ctxmenu.cursorSizeSlider);
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.invertMaskLabel);
@ -644,6 +796,7 @@ const img2imgTool = () =>
// Start Listeners
mouse.listen.world.onmousemove.on(state.mousemovecb);
mouse.listen.world.onwheel.on(state.wheelcb);
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
mouse.listen.world.btn.right.onclick.on(state.erasecb);
@ -653,6 +806,7 @@ const img2imgTool = () =>
(state, opt) => {
// Clear Listeners
mouse.listen.world.onmousemove.clear(state.mousemovecb);
mouse.listen.world.onwheel.clear(state.wheelcb);
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
@ -661,6 +815,11 @@ const img2imgTool = () =>
},
{
init: (state) => {
state.config = {
cursorSizeScrollSpeed: 1,
};
state.cursorSize = 512;
state.snapToGrid = true;
state.invertMask = true;
state.fullResolution = false;
@ -669,45 +828,63 @@ const img2imgTool = () =>
state.keepBorderSize = 64;
state.mousemovecb = (evn) => {
state.erasePrevReticle = () =>
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
_reticle_draw(evn, state.snapToGrid);
state.mousemovecb = (evn) => {
state.erasePrevReticle();
state.erasePrevReticle = _reticle_draw(evn, state);
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// Resolution
const request = {
width: stableDiffusionData.width,
height: stableDiffusionData.height,
};
// For displaying border mask
const auxCanvas = document.createElement("canvas");
auxCanvas.width = bb.w;
auxCanvas.height = bb.h;
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
if (state.keepBorderSize > 0) {
auxCtx.fillStyle = "#6A6AFF7F";
auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h);
auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize);
auxCtx.fillStyle = "#6A6AFF30";
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
auxCtx.fillRect(
bb.w - state.keepBorderSize,
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
bb.h
request.height
);
auxCtx.fillRect(
0,
bb.h - state.keepBorderSize,
bb.w,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
ovCtx.drawImage(
auxCanvas,
0,
0,
request.width,
request.height,
bb.x,
bb.y,
bb.w,
bb.h
);
}
const tmp = ovCtx.globalAlpha;
ovCtx.globalAlpha = 0.4;
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
ovCtx.globalAlpha = tmp;
};
state.wheelcb = (evn) => {
_dream_onwheel(evn, state);
};
state.dreamcb = (evn) => {
dream_img2img_callback(evn, state);
@ -717,6 +894,23 @@ const img2imgTool = () =>
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Cursor Size Slider
const cursorSizeSlider = _toolbar_input.slider(
state,
"cursorSize",
"Cursor Size",
{
min: 0,
max: 2048,
step: 128,
textStep: 2,
}
);
state.setCursorSize = cursorSizeSlider.setValue;
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
// Snap To Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
@ -746,9 +940,12 @@ const img2imgTool = () =>
state,
"denoisingStrength",
"Denoising Strength",
0,
1,
0.05
{
min: 0,
max: 1,
step: 0.05,
textStep: 0.01,
}
).slider;
// Border Mask Size Slider
@ -756,12 +953,16 @@ const img2imgTool = () =>
state,
"keepBorderSize",
"Keep Border Size",
0,
128,
1
{
min: 0,
max: 128,
step: 8,
textStep: 1,
}
).slider;
}
menu.appendChild(state.ctxmenu.cursorSizeSlider);
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.invertMaskLabel);

View file

@ -52,11 +52,47 @@ const _mask_brush_erase_callback = (evn, state) => {
maskPaintCtx.stroke();
};
const _paint_mb_cursor = (state) => {
const v = state.brushSize;
state.cursorLayer.resize(v + 20, v + 20);
const ctx = state.cursorLayer.ctx;
ctx.clearRect(0, 0, v + 20, v + 20);
ctx.beginPath();
ctx.arc(
(v + 20) / 2,
(v + 20) / 2,
state.brushSize / 2,
0,
2 * Math.PI,
true
);
ctx.fillStyle = "#FFFFFF50";
ctx.fill();
if (state.preview) {
ctx.strokeStyle = "#000F";
ctx.setLineDash([4, 2]);
ctx.stroke();
ctx.setLineDash([]);
}
};
const maskBrushTool = () =>
toolbar.registerTool(
"res/icons/paintbrush.svg",
"Mask Brush",
(state, opt) => {
// New layer for the cursor
state.cursorLayer = imageCollection.registerLayer(null, {
after: maskPaintLayer,
bb: {x: 0, y: 0, w: state.brushSize + 20, h: state.brushSize + 20},
});
_paint_mb_cursor(state);
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.world.pos});
@ -73,6 +109,10 @@ const maskBrushTool = () =>
setMask("neutral");
},
(state, opt) => {
// Don't want to keep hogging resources
imageCollection.deleteLayer(state.cursorLayer);
state.cursorLayer = null;
// Clear Listeners
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.onwheel.clear(state.wheelcb);
@ -104,21 +144,22 @@ const maskBrushTool = () =>
state.preview = false;
state.movecb = (evn) => {
// draw big translucent white blob cursor
state.clearPrevCursor = () =>
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
ovCtx.beginPath();
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
ovCtx.fillStyle = "#FFFFFF50";
ovCtx.fill();
state.movecb = (evn) => {
state.cursorLayer.moveTo(
evn.x - state.brushSize / 2 - 10,
evn.y - state.brushSize / 2 - 10
);
if (state.preview) {
ovCtx.strokeStyle = "#000F";
ovCtx.setLineDash([4, 2]);
ovCtx.stroke();
ovCtx.setLineDash([]);
}
state.clearPrevCursor = () =>
ovCtx.clearRect(
evn.x - state.brushSize / 2 - 10,
evn.y - state.brushSize / 2 - 10,
evn.x + state.brushSize / 2 + 10,
evn.y + state.brushSize / 2 + 10
);
};
state.wheelcb = (evn) => {
@ -127,7 +168,6 @@ const maskBrushTool = () =>
state.brushSize -
Math.floor(state.config.brushScrollSpeed * evn.delta)
);
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(evn);
}
};
@ -142,9 +182,16 @@ const maskBrushTool = () =>
state,
"brushSize",
"Brush Size",
state.config.minBrushSize,
state.config.maxBrushSize,
1
{
min: state.config.minBrushSize,
max: state.config.maxBrushSize,
step: 5,
textStep: 1,
cb: (v) => {
if (!state.cursorLayer) return;
_paint_mb_cursor(state);
},
}
);
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
state.setBrushSize = brushSizeSlider.setValue;
@ -174,9 +221,11 @@ const maskBrushTool = () =>
if (previewMaskButton.classList.contains("active")) {
maskPaintCanvas.classList.remove("opaque");
state.preview = false;
_paint_mb_cursor(state);
} else {
maskPaintCanvas.classList.add("opaque");
state.preview = true;
_paint_mb_cursor(state);
}
previewMaskButton.classList.toggle("active");
};

View file

@ -54,7 +54,9 @@ const selectTransformTool = () =>
state.snapToGrid = true;
state.keepAspectRatio = true;
state.useClipboard = !!navigator.clipboard.write; // Use it by default if supported
state.useClipboard = !!(
navigator.clipboard && navigator.clipboard.write
); // Use it by default if supported
state.original = null;
state.dragging = null;
@ -192,8 +194,8 @@ const selectTransformTool = () =>
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
// Update scale
@ -337,8 +339,8 @@ const selectTransformTool = () =>
let ix = evn.ix;
let iy = evn.iy;
if (state.snapToGrid) {
ix += snap(evn.ix, true, 64);
iy += snap(evn.iy, true, 64);
ix += snap(evn.ix, 0, 64);
iy += snap(evn.iy, 0, 64);
}
// If is selected, check if drag is in handles/body and act accordingly
@ -368,8 +370,8 @@ const selectTransformTool = () =>
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
// If we are scaling, stop scaling and do some handler magic
@ -489,10 +491,11 @@ const selectTransformTool = () =>
// Send to clipboard
state.clipboard.copy.toBlob((blob) => {
const item = new ClipboardItem({"image/png": blob});
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
navigator.clipboard &&
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
});
}
};
@ -501,25 +504,25 @@ const selectTransformTool = () =>
state.ctrlvcb = (evn) => {
if (state.useClipboard) {
// If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required)
navigator.clipboard.read().then((items) => {
console.info(items[0]);
for (const item of items) {
for (const type of item.types) {
if (type.startsWith("image/")) {
item.getType(type).then((blob) => {
// Converts blob to image
const url = window.URL || window.webkitURL;
const image = document.createElement("img");
image.src = url.createObjectURL(file);
tools.stamp.enable({
image,
back: tools.selecttransform.enable,
navigator.clipboard &&
navigator.clipboard.read().then((items) => {
for (const item of items) {
for (const type of item.types) {
if (type.startsWith("image/")) {
item.getType(type).then((blob) => {
// Converts blob to image
const url = window.URL || window.webkitURL;
const image = document.createElement("img");
image.src = url.createObjectURL(file);
tools.stamp.enable({
image,
back: tools.selecttransform.enable,
});
});
});
}
}
}
}
});
});
} else if (state.clipboard.copy) {
// Use internal clipboard
const image = document.createElement("img");
@ -563,7 +566,7 @@ const selectTransformTool = () =>
"Use clipboard"
);
state.ctxmenu.useClipboardLabel = clipboardCheckbox.label;
if (!navigator.clipboard.write)
if (!(navigator.clipboard && navigator.clipboard.write))
clipboardCheckbox.checkbox.disabled = true; // Disable if not available
// Some useful actions to do with selection

View file

@ -19,7 +19,8 @@ const stampTool = () =>
state.addResource(
opt.name || "Clipboard",
opt.image,
opt.temporary === undefined ? true : opt.temporary
opt.temporary === undefined ? true : opt.temporary,
false
);
state.ctxmenu.uploadButton.disabled = true;
state.back = opt.back || null;
@ -56,8 +57,14 @@ const stampTool = () =>
state.lastMouseMove = {x: 0, y: 0};
state.selectResource = (resource) => {
if (state.ctxmenu.uploadButton.disabled) return;
state.selectResource = (resource, nolock = true) => {
if (nolock && state.ctxmenu.uploadButton.disabled) return;
console.debug(
`[stamp] Selecting Resource '${resource && resource.name}'[${
resource && resource.id
}]`
);
const resourceWrapper = resource && resource.dom.wrapper;
@ -83,8 +90,38 @@ const stampTool = () =>
if (state.loaded) state.movecb(state.lastMouseMove);
};
// Synchronizes resources array with the DOM
// Synchronizes resources array with the DOM and Local Storage
const syncResources = () => {
// Saves to local storage
try {
localStorage.setItem(
"tools.stamp.resources",
JSON.stringify(
state.resources
.filter((resource) => !resource.temporary)
.map((resource) => {
const canvas = document.createElement("canvas");
canvas.width = resource.image.width;
canvas.height = resource.image.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(resource.image, 0, 0);
return {
id: resource.id,
name: resource.name,
src: canvas.toDataURL(),
};
})
)
);
} catch (e) {
console.warn(
"[stamp] Failed to synchronize resources with local storage"
);
console.warn(e);
}
// Creates DOM elements when needed
state.resources.forEach((resource) => {
if (
@ -93,12 +130,15 @@ const stampTool = () =>
)
) {
console.debug(
`Creating resource element 'resource-${resource.id}'`
`[stamp] Creating Resource Element [resource-${resource.id}]`
);
const resourceWrapper = document.createElement("div");
resourceWrapper.id = `resource-${resource.id}`;
resourceWrapper.textContent = resource.name;
resourceWrapper.classList.add("resource");
const resourceTitle = document.createElement("span");
resourceTitle.textContent = resource.name;
resourceTitle.classList.add("resource-title");
resourceWrapper.appendChild(resourceTitle);
resourceWrapper.addEventListener("click", () =>
state.selectResource(resource)
@ -112,6 +152,41 @@ const stampTool = () =>
state.ctxmenu.previewPane.style.display = "none";
});
// Add action buttons
const actionArray = document.createElement("div");
actionArray.classList.add("actions");
const renameButton = document.createElement("button");
renameButton.addEventListener("click", () => {
const name = prompt("Rename your resource:", resource.name);
if (name) {
resource.name = name;
resourceTitle.textContent = name;
syncResources();
}
});
renameButton.title = "Rename Resource";
renameButton.appendChild(document.createElement("div"));
renameButton.classList.add("rename-btn");
const trashButton = document.createElement("button");
trashButton.addEventListener(
"click",
(evn) => {
evn.stopPropagation();
state.ctxmenu.previewPane.style.display = "none";
state.deleteResource(resource.id);
},
{passive: false}
);
trashButton.title = "Delete Resource";
trashButton.appendChild(document.createElement("div"));
trashButton.classList.add("delete-btn");
actionArray.appendChild(renameButton);
actionArray.appendChild(trashButton);
resourceWrapper.appendChild(actionArray);
state.ctxmenu.resourceList.appendChild(resourceWrapper);
resource.dom = {wrapper: resourceWrapper};
}
@ -124,15 +199,20 @@ const stampTool = () =>
elements.forEach((element) => {
let remove = true;
state.resources.some((resource) => {
if (element.id.endsWith(resource.id)) remove = false;
if (element.id.endsWith(resource.id)) {
remove = false;
}
});
if (remove) state.ctxmenu.resourceList.removeChild(element);
if (remove) {
console.debug(`[stamp] Sync Removing Element [${element.id}]`);
state.ctxmenu.resourceList.removeChild(element);
}
});
};
// Adds a image resource (temporary allows only one draw, used for pasting)
state.addResource = (name, image, temporary = false) => {
state.addResource = (name, image, temporary = false, nolock = true) => {
const id = guid();
const resource = {
id,
@ -140,19 +220,28 @@ const stampTool = () =>
image,
temporary,
};
console.info(`[stamp] Adding Resource '${name}'[${id}]`);
state.resources.push(resource);
syncResources();
// Select this resource
state.selectResource(resource);
state.selectResource(resource, nolock);
return resource;
};
// Deletes a resource (Yes, functionality is here, but we don't have an UI for this yet)
// Used for temporary images too
state.deleteResource = (id) => {
state.resources = state.resources.filter((v) => v.id !== id);
const resourceIndex = state.resources.findIndex((v) => v.id === id);
const resource = state.resources[resourceIndex];
if (state.selected === resource) state.selected = null;
console.info(
`[stamp] Deleting Resource '${resource.name}'[${resource.id}]`
);
state.resources.splice(resourceIndex, 1);
syncResources();
};
@ -161,8 +250,8 @@ const stampTool = () =>
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
state.lastMouseMove = evn;
@ -190,8 +279,8 @@ const stampTool = () =>
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
const resource = state.selected;
@ -203,7 +292,9 @@ const stampTool = () =>
y,
});
if (resource.temporary) state.deleteResource(resource.id);
if (resource.temporary) {
state.deleteResource(resource.id);
}
}
if (state.back) {
@ -268,7 +359,7 @@ const stampTool = () =>
const image = document.createElement("img");
image.src = url.createObjectURL(file);
state.addResource(file.name, image, false);
image.onload = () => state.addResource(file.name, image, false);
}
});
@ -321,6 +412,29 @@ const stampTool = () =>
state.ctxmenu.previewPane = previewPane;
state.ctxmenu.resourceManager = resourceManager;
state.ctxmenu.resourceList = resourceList;
// Performs resource fetch from local storage
{
const storageResources = localStorage.getItem(
"tools.stamp.resources"
);
if (storageResources) {
const parsed = JSON.parse(storageResources);
state.resources.push(
...parsed.map((resource) => {
const image = document.createElement("img");
image.src = resource.src;
return {
id: resource.id,
name: resource.name,
image,
};
})
);
syncResources();
}
}
}
},
populateContextMenu: (menu, state) => {

5
res/icons/brush.svg Normal file
View 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">
<path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"></path>
<path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"></path>
</svg>

After

Width:  |  Height:  |  Size: 413 B

4
res/icons/check.svg Normal file
View file

@ -0,0 +1,4 @@
<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">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 237 B

5
res/icons/edit.svg Normal file
View 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">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 344 B

6
res/icons/trash.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>

After

Width:  |  Height:  |  Size: 330 B