diff --git a/index.html b/index.html
index e091f19..705b431 100644
--- a/index.html
+++ b/index.html
@@ -318,6 +318,7 @@
type="text/javascript">
+
diff --git a/js/global.js b/js/global.js
new file mode 100644
index 0000000..415c377
--- /dev/null
+++ b/js/global.js
@@ -0,0 +1,22 @@
+/**
+ * Stores global variables without polluting the global namespace.
+ */
+
+const global = {
+ // Connection
+ _connection: "offline",
+ set connection(v) {
+ this._connection = v;
+
+ toolbar &&
+ toolbar.currentTool &&
+ toolbar.currentTool.state.redraw &&
+ toolbar.currentTool.state.redraw();
+ },
+ get connection() {
+ return this._connection;
+ },
+
+ // If there is a selected input
+ hasActiveInput: false,
+};
diff --git a/js/index.js b/js/index.js
index 97a0821..4b30cbb 100644
--- a/js/index.js
+++ b/js/index.js
@@ -188,6 +188,7 @@ function startup() {
}
function setFixedHost(h, changePromptMessage) {
+ console.info(`[index] Fixed host to '${h}'`);
const hostInput = document.getElementById("host");
host = h;
hostInput.value = h;
@@ -340,7 +341,11 @@ async function testHostConnection() {
},
};
- statuses[status] && statuses[status]();
+ statuses[status] &&
+ (() => {
+ statuses[status]();
+ global.connection = status;
+ })();
};
setConnectionStatus("before");
@@ -411,7 +416,7 @@ async function testHostConnection() {
return status;
};
- await checkConnection(true);
+ await checkConnection(!urlParams.has("noprompt"));
// On click, attempt to refresh
connectionIndicator.onclick = async () => {
@@ -457,6 +462,8 @@ function clearPaintedMask() {
function march(bb, options = {}) {
defaultOpt(options, {
+ title: null,
+ titleStyle: "#FFF5",
style: "#FFFF",
width: "2px",
filter: null,
@@ -471,6 +478,7 @@ function march(bb, options = {}) {
// Get temporary layer to draw marching ants
const layer = imageCollection.registerLayer(null, {
bb: expanded,
+ category: "display",
});
layer.canvas.style.imageRendering = "pixelated";
let offset = 0;
@@ -490,6 +498,16 @@ function drawMarchingAnts(ctx, bb, offset, options) {
ctx.save();
ctx.clearRect(0, 0, bb.w + 2, bb.h + 2);
+
+ // Draw Tool Name
+ if (bb.h > 40 && options.title) {
+ ctx.font = `bold 20px Open Sans`;
+
+ ctx.textAlign = "left";
+ ctx.fillStyle = options.titleStyle;
+ ctx.fillText(options.title, 10, 30, bb.w);
+ }
+
ctx.strokeStyle = options.style;
ctx.strokeWidth = options.width;
ctx.filter = options.filter;
@@ -920,6 +938,12 @@ async function getSamplers() {
try {
const response = await fetch(url);
const data = await response.json();
+
+ samplerAutoComplete.onchange.on(({value}) => {
+ stableDiffusionData.sampler_index = value;
+ localStorage.setItem("openoutpaint/sampler", value);
+ });
+
samplerAutoComplete.options = data.map((sampler) => ({
name: sampler.name,
value: sampler.name,
@@ -932,11 +956,7 @@ async function getSamplers() {
samplerAutoComplete.value = data[0].name;
localStorage.setItem("openoutpaint/sampler", samplerAutoComplete.value);
}
-
- samplerAutoComplete.onchange.on(({value}) => {
- stableDiffusionData.sampler_index = value;
- localStorage.setItem("openoutpaint/sampler", value);
- });
+ stableDiffusionData.sampler_index = samplerAutoComplete.value;
} catch (e) {
console.warn("[index] Failed to fetch samplers");
console.warn(e);
diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js
index 7346aad..c98ffba 100644
--- a/js/initalize/layers.populate.js
+++ b/js/initalize/layers.populate.js
@@ -20,20 +20,25 @@ const imageCollection = layers.registerCollection(
const bgLayer = imageCollection.registerLayer("bg", {
name: "Background",
+ category: "background",
});
const imgLayer = imageCollection.registerLayer("image", {
name: "Image",
+ category: "image",
ctxOptions: {desynchronized: true},
});
const maskPaintLayer = imageCollection.registerLayer("mask", {
name: "Mask Paint",
+ category: "mask",
ctxOptions: {desynchronized: true},
});
const ovLayer = imageCollection.registerLayer("overlay", {
name: "Overlay",
+ category: "display",
});
const debugLayer = imageCollection.registerLayer("debug", {
name: "Debug Layer",
+ category: "display",
});
const imgCanvas = imgLayer.canvas; // where dreams go
@@ -237,9 +242,28 @@ mouse.registerContext(
ctx.coords.pos.x = Math.round(layerCoords.x);
ctx.coords.pos.y = Math.round(layerCoords.y);
},
- {target: imageCollection.inputElement}
+ {
+ target: imageCollection.inputElement,
+ validate: (evn) => {
+ if (!global.hasActiveInput || evn.type === "mousemove") return true;
+ return false;
+ },
+ }
);
+// Redraw on active input state change
+(() => {
+ mouse.listen.window.onany.on((evn) => {
+ const activeInput = DOM.hasActiveInput();
+ if (global.hasActiveInput !== activeInput) {
+ global.hasActiveInput = activeInput;
+ toolbar.currentTool &&
+ toolbar.currentTool.state.redraw &&
+ toolbar.currentTool.state.redraw();
+ }
+ });
+})();
+
mouse.listen.window.onwheel.on((evn) => {
if (evn.evn.ctrlKey) {
evn.evn.preventDefault();
diff --git a/js/lib/input.d.js b/js/lib/input.d.js
index 07ae2b3..e9caa06 100644
--- a/js/lib/input.d.js
+++ b/js/lib/input.d.js
@@ -42,6 +42,7 @@
* An object for mouse event listeners
*
* @typedef MouseListenerContext
+ * @property {Observer} onany A listener for any mouse events
* @property {Observer} onmousemove A mouse move handler
* @property {Observer} onwheel A mouse wheel handler
* @property {Record} btn Button handlers
@@ -67,6 +68,7 @@
* @property {ContextMoveTransformer} onmove The coordinate transform callback
* @property {(evn) => void} onany A function to be run on any event
* @property {?HTMLElement} target The target
+ * @property {(evn) => boolean} validate A function to be check if we will process an event
* @property {MouseCoordContext} coords Coordinates object
* @property {MouseListenerContext} listen Listeners object
*/
diff --git a/js/lib/input.js b/js/lib/input.js
index b431047..4a469e3 100644
--- a/js/lib/input.js
+++ b/js/lib/input.js
@@ -63,16 +63,16 @@ const mouse = {
* @param {ContextMoveTransformer} onmove The function to perform coordinate transform
* @param {object} options Extra options
* @param {HTMLElement} [options.target=null] Target filtering
+ * @param {(evn: any) => boolean} [options.validate] Checks if we will process this event or not
* @param {Record} [options.buttons={0: "left", 1: "middle", 2: "right"}] Custom button mapping
- * @param {(evn) => void} [options.genericcb=null] Function that will be run for all events (useful for preventDefault)
* @returns {MouseContext}
*/
registerContext: (name, onmove, options = {}) => {
// Options
defaultOpt(options, {
target: null,
+ validate: () => true,
buttons: {0: "left", 1: "middle", 2: "right"},
- genericcb: null,
});
// Context information
@@ -81,8 +81,8 @@ const mouse = {
id: guid(),
name,
onmove,
- onany: options.genericcb,
target: options.target,
+ validate: options.validate,
buttons: options.buttons,
};
@@ -102,12 +102,27 @@ const mouse = {
};
// Listeners
+ const onany = new Observer();
+
mouse.listen[name] = {
+ onany,
onwheel: new Observer(),
onmousemove: new Observer(),
btn: {},
};
+ // Always process onany events first
+ mouse.listen[name].onwheel.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].onmousemove.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+
// Button specific items
Object.keys(options.buttons).forEach((index) => {
const button = options.buttons[index];
@@ -115,6 +130,48 @@ const mouse = {
mouse.listen[name].btn[button] = _mouse_observers(
`mouse.listen[${name}].btn[${button}]`
);
+
+ // Always process onany events first
+ mouse.listen[name].btn[button].onclick.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].ondclick.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].ondragstart.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].ondrag.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].ondragend.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].onpaintstart.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].onpaint.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
+ mouse.listen[name].btn[button].onpaintend.on(
+ async (evn, state) => await onany.emit(evn, state),
+ Infinity,
+ true
+ );
});
// Add to context
@@ -183,11 +240,13 @@ window.addEventListener(
mouse.buttons[evn.button] = time;
- mouse._contexts.forEach(({target, name, buttons, onany}) => {
+ mouse._contexts.forEach(({target, name, buttons, validate}) => {
const key = buttons[evn.button];
- if ((!target || target === evn.target) && key) {
- onany && onany();
-
+ if (
+ (!target || target === evn.target) &&
+ key &&
+ (!validate || validate(evn))
+ ) {
mouse.coords[name].dragging[key] = {};
mouse.coords[name].dragging[key].target = evn.target;
Object.assign(mouse.coords[name].dragging[key], mouse.coords[name].pos);
@@ -214,14 +273,14 @@ window.addEventListener(
(evn) => {
const time = performance.now();
- mouse._contexts.forEach(({target, name, buttons, onany}) => {
+ mouse._contexts.forEach(({target, name, buttons, validate}) => {
const key = buttons[evn.button];
if (
(!target || target === evn.target) &&
key &&
- mouse.coords[name].dragging[key]
+ mouse.coords[name].dragging[key] &&
+ (!validate || validate(evn))
) {
- onany && onany();
const start = {
x: mouse.coords[name].dragging[key].x,
y: mouse.coords[name].dragging[key].y,
@@ -292,7 +351,10 @@ window.addEventListener(
const target = context.target;
const name = context.name;
- if (!target || target === evn.target) {
+ if (
+ !target ||
+ (target === evn.target && (!context.validate || context.validate(evn)))
+ ) {
context.onmove(evn, context);
mouse.listen[name].onmousemove.emit({
@@ -378,19 +440,21 @@ window.addEventListener(
window.addEventListener(
"wheel",
(evn) => {
- mouse._contexts.forEach(({name}) => {
- mouse.listen[name].onwheel.emit({
- target: evn.target,
- delta: evn.deltaY,
- deltaX: evn.deltaX,
- deltaY: evn.deltaY,
- deltaZ: evn.deltaZ,
- mode: evn.deltaMode,
- x: mouse.coords[name].pos.x,
- y: mouse.coords[name].pos.y,
- evn,
- timestamp: performance.now(),
- });
+ mouse._contexts.forEach(({name, target, validate}) => {
+ if (!target || (target === evn.target && (!validate || validate(evn)))) {
+ mouse.listen[name].onwheel.emit({
+ target: evn.target,
+ delta: evn.deltaY,
+ deltaX: evn.deltaX,
+ deltaY: evn.deltaY,
+ deltaZ: evn.deltaZ,
+ mode: evn.deltaMode,
+ x: mouse.coords[name].pos.x,
+ y: mouse.coords[name].pos.y,
+ evn,
+ timestamp: performance.now(),
+ });
+ }
});
},
{passive: false}
diff --git a/js/lib/layers.js b/js/lib/layers.js
index 779cc19..3c513cd 100644
--- a/js/lib/layers.js
+++ b/js/lib/layers.js
@@ -224,9 +224,6 @@ const layers = {
// Input element (overlay element for input handling)
const inputel = document.createElement("div");
inputel.id = `collection-input-${id}`;
- inputel.addEventListener("mouseover", (evn) => {
- document.activeElement.blur();
- });
inputel.classList.add("collection-input-overlay");
element.appendChild(inputel);
@@ -340,6 +337,7 @@ const layers = {
* @param {object} options
* @param {string} options.name
* @param {?BoundingBox} options.bb
+ * @param {string} [options.category]
* @param {{w: number, h: number}} options.resolution
* @param {?string} options.group
* @param {object} options.after
@@ -362,6 +360,9 @@ const layers = {
h: collection.size.h,
},
+ // Category of the layer
+ category: null,
+
// Resolution for layer
resolution: null,
@@ -451,6 +452,7 @@ const layers = {
key,
name: options.name,
full,
+ category: options.category,
state: new Proxy(
{visible: true},
@@ -494,6 +496,10 @@ const layers = {
return this._collection.origin;
},
+ get hidden() {
+ return !this.state.visible;
+ },
+
/** Our canvas */
canvas,
ctx,
diff --git a/js/lib/util.js b/js/lib/util.js
index f6d5c60..22b3c1c 100644
--- a/js/lib/util.js
+++ b/js/lib/util.js
@@ -106,9 +106,9 @@ class Observer {
* Sends a message to all observers
*
* @param {T} msg The message to send to the observers
+ * @param {any} state The initial state
*/
- async emit(msg) {
- const state = {};
+ async emit(msg, state = {}) {
const promises = [];
for (const {handler, wait} of this._handlers) {
const run = async () => {
@@ -128,6 +128,25 @@ class Observer {
}
}
+/**
+ * Static DOM utility functions
+ */
+class DOM {
+ static inputTags = new Set(["input", "textarea"]);
+
+ /**
+ * Checks if there is an active input
+ *
+ * @returns Whether there is currently an active input
+ */
+ static hasActiveInput() {
+ return (
+ document.activeElement &&
+ this.inputTags.has(document.activeElement.tagName.toLowerCase())
+ );
+ }
+}
+
/**
* Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f]
*
diff --git a/js/ui/floating/layers.js b/js/ui/floating/layers.js
index ebed104..6682453 100644
--- a/js/ui/floating/layers.js
+++ b/js/ui/floating/layers.js
@@ -3,10 +3,17 @@
*/
const uil = {
+ /** @type {Observer<{uilayer: UILayer}>} */
+ onactive: new Observer(),
+
_ui_layer_list: document.getElementById("layer-list"),
layers: [],
_active: null,
set active(v) {
+ this.onactive.emit({
+ uilayer: v,
+ });
+
Array.from(this._ui_layer_list.children).forEach((child) => {
child.classList.remove("active");
});
@@ -188,6 +195,7 @@ const uil = {
_addLayer(group, name) {
const layer = imageCollection.registerLayer(null, {
name,
+ category: "user",
after:
(this.layers.length > 0 && this.layers[this.layers.length - 1].layer) ||
bgLayer,
@@ -285,11 +293,13 @@ const uil = {
* @param {BoundingBox} bb The bouding box to get visible data from
* @param {object} [options] Options
* @param {boolean} [options.includeBg=false] Whether to include the background
+ * @param {string[]} [options.categories] Categories of layers to consider visible
* @returns {HTMLCanvasElement} The canvas element containing visible image data
*/
getVisible(bb, options = {}) {
defaultOpt(options, {
includeBg: false,
+ categories: ["user", "image"],
});
const canvas = document.createElement("canvas");
@@ -297,21 +307,14 @@ const uil = {
canvas.width = bb.w;
canvas.height = bb.h;
- if (options.includeBg)
- ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
- this.layers.forEach((layer) => {
- if (!layer.hidden)
- ctx.drawImage(
- layer.layer.canvas,
- bb.x,
- bb.y,
- bb.w,
- bb.h,
- 0,
- 0,
- bb.w,
- bb.h
- );
+
+ const categories = new Set(options.categories);
+ if (options.includeBg) categories.add("background");
+ const layers = imageCollection._layers;
+
+ layers.reduceRight((_, layer) => {
+ if (categories.has(layer.category) && !layer.hidden)
+ ctx.drawImage(layer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
});
return canvas;
@@ -336,6 +339,7 @@ commands.createCommand(
const layer = imageCollection.registerLayer(null, {
name,
+ category: "user",
after:
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
bgLayer,
diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js
index 01b4cbb..32701b3 100644
--- a/js/ui/tool/colorbrush.js
+++ b/js/ui/tool/colorbrush.js
@@ -62,16 +62,19 @@ const colorBrushTool = () =>
state.drawLayer = imageCollection.registerLayer(null, {
after: imgLayer,
+ category: "display",
ctxOptions: {willReadFrequently: true},
});
state.drawLayer.canvas.style.filter = "opacity(70%)";
state.eraseLayer = imageCollection.registerLayer(null, {
after: imgLayer,
+ category: "processing",
ctxOptions: {willReadFrequently: true},
});
state.eraseLayer.hide();
state.eraseBackup = imageCollection.registerLayer(null, {
after: imgLayer,
+ category: "processing",
});
state.eraseBackup.hide();
diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js
index 2ba3061..fa19b1a 100644
--- a/js/ui/tool/dream.js
+++ b/js/ui/tool/dream.js
@@ -24,6 +24,7 @@ const _monitorProgress = (bb, oncheck = null) => {
// Get temporary layer to draw progress bar
const layer = imageCollection.registerLayer(null, {
bb: expanded,
+ category: "display",
});
layer.canvas.style.opacity = "70%";
@@ -293,6 +294,7 @@ const _generate = async (endpoint, request, bb, options = {}) => {
// Layer for the images
const layer = imageCollection.registerLayer(null, {
after: maskPaintLayer,
+ category: "display",
});
const redraw = (url = images[at]) => {
@@ -1227,7 +1229,7 @@ const dreamTool = () =>
y += snap(evn.y, 0, 64);
}
- state.erasePrevReticle = _tool._cursor_draw(x, y);
+ state.erasePrevCursor = _tool._cursor_draw(x, y);
if (state.selection.exists) {
const bb = state.selection.bb;
@@ -1251,6 +1253,8 @@ const dreamTool = () =>
),
},
{
+ toolTextStyle:
+ global.connection === "online" ? "#FFF5" : "#F555",
reticleStyle: state.selection.inside ? "#F55" : "#FFF",
sizeTextStyle: style,
}
@@ -1278,6 +1282,7 @@ const dreamTool = () =>
h: stableDiffusionData.height,
},
{
+ toolTextStyle: global.connection === "online" ? "#FFF5" : "#F555",
sizeTextStyle: style,
}
);
@@ -1306,8 +1311,19 @@ const dreamTool = () =>
w: stableDiffusionData.width,
h: stableDiffusionData.height,
};
- dream_generate_callback(bb, resolution, state);
+
+ if (global.connection === "online") {
+ dream_generate_callback(bb, resolution, state);
+ } else {
+ const stop = march(bb, {
+ title: "offline",
+ titleStyle: "#F555",
+ style: "#F55",
+ });
+ setTimeout(stop, 2000);
+ }
state.selection.deselect();
+ state.redraw();
};
state.erasecb = (evn, estate) => {
if (state.selection.exists) {
@@ -1595,7 +1611,7 @@ const img2imgTool = () =>
y += snap(evn.y, 0, 64);
}
- state.erasePrevReticle = _tool._cursor_draw(x, y);
+ state.erasePrevCursor = _tool._cursor_draw(x, y);
// Resolution
let bb = null;
@@ -1624,6 +1640,8 @@ const img2imgTool = () =>
),
},
{
+ toolTextStyle:
+ global.connection === "online" ? "#FFF5" : "#F555",
reticleStyle: state.selection.inside ? "#F55" : "#FFF",
sizeTextStyle: style,
}
@@ -1653,6 +1671,8 @@ const img2imgTool = () =>
"Img2Img",
{w: request.width, h: request.height},
{
+ toolTextStyle:
+ global.connection === "online" ? "#FFF5" : "#F555",
sizeTextStyle: style,
}
);
@@ -1777,7 +1797,16 @@ const img2imgTool = () =>
w: stableDiffusionData.width,
h: stableDiffusionData.height,
};
- dream_img2img_callback(bb, resolution, state);
+ if (global.connection === "online") {
+ dream_img2img_callback(bb, resolution, state);
+ } else {
+ const stop = march(bb, {
+ title: "offline",
+ titleStyle: "#F555",
+ style: "#F55",
+ });
+ setTimeout(stop, 2000);
+ }
state.selection.deselect();
state.redraw();
};
diff --git a/js/ui/tool/generic.js b/js/ui/tool/generic.js
index 6de0388..56e3a6b 100644
--- a/js/ui/tool/generic.js
+++ b/js/ui/tool/generic.js
@@ -14,7 +14,7 @@ const _tool = {
* @param {string} [style.genSizeTextStyle = "#FFF5"] Style of the text for diplaying generation size
* @param {string} [style.toolTextStyle = "#FFF5"] Style of the text for the tool name
* @param {number} [style.reticleWidth = 1] Width of the line of the reticle
- * @param {string} [style.reticleStyle = "#FFF"] Style of the line of the reticle
+ * @param {string} [style.reticleStyle] Style of the line of the reticle
*
* @returns A function that erases this reticle drawing
*/
@@ -24,7 +24,7 @@ const _tool = {
genSizeTextStyle: "#FFF5",
toolTextStyle: "#FFF5",
reticleWidth: 1,
- reticleStyle: "#FFF",
+ reticleStyle: global.hasActiveInput ? "#BBF" : "#FFF",
});
const bbvp = {
@@ -110,14 +110,14 @@ const _tool = {
* @param {number} y Y world coordinate of the cursor
* @param {object} style Style of the lines of the cursor
* @param {string} [style.width = 3] Line width of the lines of the cursor
- * @param {string} [style.style = "#FFF5"] Stroke style of the lines of the cursor
+ * @param {string} [style.style] Stroke style of the lines of the cursor
*
* @returns A function that erases this cursor drawing
*/
_cursor_draw(x, y, style = {}) {
defaultOpt(style, {
width: 3,
- style: "#FFF5",
+ style: global.hasActiveInput ? "#BBF5" : "#FFF5",
});
const vpc = viewport.canvasToView(x, y);
diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js
index 0e41726..5fd2cd5 100644
--- a/js/ui/tool/select.js
+++ b/js/ui/tool/select.js
@@ -20,6 +20,9 @@ const selectTransformTool = () =>
keyboard.listen.onkeyclick.on(state.keyclickcb);
keyboard.listen.onkeydown.on(state.keydowncb);
+ // Layer system handlers
+ uil.onactive.on(state.uilayeractivecb);
+
// Registers keyboard shortcuts
keyboard.onShortcut({ctrl: true, key: "KeyC"}, state.ctrlccb);
keyboard.onShortcut({ctrl: true, key: "KeyV"}, state.ctrlvcb);
@@ -42,6 +45,8 @@ const selectTransformTool = () =>
keyboard.deleteShortcut(state.ctrlvcb, "KeyV");
keyboard.deleteShortcut(state.ctrlxcb, "KeyX");
+ uil.onactive.clear(state.uilayeractivecb);
+
// Clear any selections
state.reset();
@@ -61,6 +66,7 @@ const selectTransformTool = () =>
state.useClipboard = !!(
navigator.clipboard && navigator.clipboard.write
); // Use it by default if supported
+ state.selectionPeekOpacity = 40;
state.original = null;
state.dragging = null;
@@ -78,22 +84,33 @@ const selectTransformTool = () =>
// Some things to easy request for a redraw
state.lastMouseTarget = null;
- state.lastMouseMove = null;
+ state.lastMouseMove = {x: 0, y: 0};
state.redraw = () => {
ovLayer.clear();
state.movecb(state.lastMouseMove);
};
+ state.uilayeractivecb = ({uilayer}) => {
+ if (state.originalDisplayLayer) {
+ state.originalDisplayLayer.moveAfter(uilayer.layer);
+ }
+ };
+
// Clears selection and make things right
- state.reset = () => {
- if (state.selected)
- uil.ctx.drawImage(
+ state.reset = (erase = false) => {
+ if (state.selected && !erase)
+ state.originalLayer.ctx.drawImage(
state.original.image,
state.original.x,
state.original.y
);
+ if (state.originalDisplayLayer) {
+ imageCollection.deleteLayer(state.originalDisplayLayer);
+ state.originalDisplayLayer = null;
+ }
+
if (state.dragging) state.dragging = null;
else state.selected = null;
@@ -189,6 +206,7 @@ const selectTransformTool = () =>
state.movecb = (evn) => {
ovLayer.clear();
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+ state.erasePrevCursor && state.erasePrevCursor();
imageCollection.inputElement.style.cursor = "auto";
state.lastMouseTarget = evn.target;
state.lastMouseMove = evn;
@@ -254,6 +272,8 @@ const selectTransformTool = () =>
};
// Draw Image
+ ovCtx.save();
+ ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`;
ovCtx.drawImage(
state.selected.image,
0,
@@ -265,6 +285,22 @@ const selectTransformTool = () =>
state.selected.w,
state.selected.h
);
+ ovCtx.restore();
+
+ state.originalDisplayLayer.clear();
+ state.originalDisplayLayer.ctx.save();
+ state.originalDisplayLayer.ctx.drawImage(
+ state.selected.image,
+ 0,
+ 0,
+ state.selected.image.width,
+ state.selected.image.height,
+ state.selected.x,
+ state.selected.y,
+ state.selected.w,
+ state.selected.h
+ );
+ state.originalDisplayLayer.ctx.restore();
// Draw selection box
uiCtx.strokeStyle = "#FFF";
@@ -320,15 +356,7 @@ const selectTransformTool = () =>
}
// Draw current cursor location
- uiCtx.lineWidth = 3;
- uiCtx.strokeStyle = "#FFF";
-
- uiCtx.beginPath();
- uiCtx.moveTo(vpc.x, vpc.y + 10);
- uiCtx.lineTo(vpc.x, vpc.y - 10);
- uiCtx.moveTo(vpc.x + 10, vpc.y);
- uiCtx.lineTo(vpc.x - 10, vpc.y);
- uiCtx.stroke();
+ state.erasePrevCursor = _tool._cursor_draw(x, y);
uiCtx.restore();
};
@@ -337,7 +365,8 @@ const selectTransformTool = () =>
state.clickcb = (evn) => {
if (
!state.original ||
- (state.original.x === state.selected.x &&
+ (state.originalLayer === uil.layer &&
+ state.original.x === state.selected.x &&
state.original.y === state.selected.y &&
state.original.w === state.selected.w &&
state.original.h === state.selected.h)
@@ -348,16 +377,15 @@ const selectTransformTool = () =>
// If something is selected, commit changes to the canvas
if (state.selected) {
- uil.ctx.drawImage(
+ state.originalLayer.ctx.drawImage(
state.selected.image,
state.original.x,
state.original.y
);
- commands.runCommand(
- "eraseImage",
- "Image Transform Erase",
- state.original
- );
+ commands.runCommand("eraseImage", "Image Transform Erase", {
+ ...state.original,
+ ctx: state.originalLayer.ctx,
+ });
commands.runCommand("drawImage", "Image Transform Draw", {
image: state.selected.image,
x: Math.round(state.selected.x),
@@ -365,10 +393,7 @@ const selectTransformTool = () =>
w: Math.round(state.selected.w),
h: Math.round(state.selected.h),
});
- state.original = null;
- state.selected = null;
-
- state.redraw();
+ state.reset(true);
}
};
@@ -451,6 +476,11 @@ const selectTransformTool = () =>
x,
y
);
+ state.originalLayer = uil.layer;
+ state.originalDisplayLayer = imageCollection.registerLayer(null, {
+ after: uil.layer,
+ category: "select-display",
+ });
// Cut out selected portion of the image for manipulation
const cvs = document.createElement("canvas");
@@ -622,6 +652,22 @@ const selectTransformTool = () =>
if (!(navigator.clipboard && navigator.clipboard.write))
clipboardCheckbox.checkbox.disabled = true; // Disable if not available
+ // Selection Peek Opacity
+ state.ctxmenu.selectionPeekOpacitySlider = _toolbar_input.slider(
+ state,
+ "selectionPeekOpacity",
+ "Peek Opacity",
+ {
+ min: 0,
+ max: 100,
+ step: 10,
+ textStep: 1,
+ cb: () => {
+ state.redraw();
+ },
+ }
+ ).slider;
+
// Some useful actions to do with selection
const actionArray = document.createElement("div");
actionArray.classList.add("button-array");
@@ -629,7 +675,7 @@ const selectTransformTool = () =>
// Save button
const saveSelectionButton = document.createElement("button");
saveSelectionButton.classList.add("button", "tool");
- saveSelectionButton.textContent = "Save";
+ saveSelectionButton.textContent = "Save Sel.";
saveSelectionButton.title = "Saves Selection";
saveSelectionButton.onclick = () => {
downloadCanvas({
@@ -655,25 +701,72 @@ const selectTransformTool = () =>
actionArray.appendChild(saveSelectionButton);
actionArray.appendChild(createResourceButton);
+ // Some useful actions to do with selection
+ const visibleActionArray = document.createElement("div");
+ visibleActionArray.classList.add("button-array");
+
+ // Save Visible button
+ const saveVisibleSelectionButton = document.createElement("button");
+ saveVisibleSelectionButton.classList.add("button", "tool");
+ saveVisibleSelectionButton.textContent = "Save Vis.";
+ saveVisibleSelectionButton.title = "Saves Visible Selection";
+ saveVisibleSelectionButton.onclick = () => {
+ const canvas = uil.getVisible(state.selected, {
+ categories: ["image", "user", "select-display"],
+ });
+ downloadCanvas({
+ cropToContent: false,
+ canvas,
+ });
+ };
+
+ // Save Visible as Resource Button
+ const createVisibleResourceButton = document.createElement("button");
+ createVisibleResourceButton.classList.add("button", "tool");
+ createVisibleResourceButton.textContent = "Vis. to Res.";
+ createVisibleResourceButton.title =
+ "Saves Visible Selection as a Resource";
+ createVisibleResourceButton.onclick = () => {
+ const canvas = uil.getVisible(state.selected, {
+ categories: ["image", "user", "select-display"],
+ });
+ const image = document.createElement("img");
+ image.src = canvas.toDataURL();
+ image.onload = () => {
+ tools.stamp.state.addResource("Selection Resource", image);
+ tools.stamp.enable();
+ };
+ };
+
+ visibleActionArray.appendChild(saveVisibleSelectionButton);
+ visibleActionArray.appendChild(createVisibleResourceButton);
+
// Disable buttons (if nothing is selected)
state.ctxmenu.disableButtons = () => {
saveSelectionButton.disabled = true;
createResourceButton.disabled = true;
+ saveVisibleSelectionButton.disabled = true;
+ createVisibleResourceButton.disabled = true;
};
// Disable buttons (if something is selected)
state.ctxmenu.enableButtons = () => {
saveSelectionButton.disabled = "";
createResourceButton.disabled = "";
+ saveVisibleSelectionButton.disabled = "";
+ createVisibleResourceButton.disabled = "";
};
state.ctxmenu.actionArray = actionArray;
+ state.ctxmenu.visibleActionArray = visibleActionArray;
}
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.keepAspectRatioLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.useClipboardLabel);
+ menu.appendChild(state.ctxmenu.selectionPeekOpacitySlider);
menu.appendChild(state.ctxmenu.actionArray);
+ menu.appendChild(state.ctxmenu.visibleActionArray);
},
shortcut: "S",
}
diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js
index 5aa1811..2699841 100644
--- a/js/ui/tool/stamp.js
+++ b/js/ui/tool/stamp.js
@@ -151,18 +151,23 @@ const stampTool = () =>
);
const resourceWrapper = document.createElement("div");
resourceWrapper.id = `resource-${resource.id}`;
+ resourceWrapper.title = resource.name;
resourceWrapper.classList.add("resource", "list-item");
const resourceTitle = document.createElement("input");
resourceTitle.value = resource.name;
- resourceTitle.title = resource.name;
resourceTitle.style.pointerEvents = "none";
resourceTitle.addEventListener("change", () => {
resource.name = resourceTitle.value;
resource.dirty = true;
- resourceTitle.title = resourceTitle.value;
+ resourceWrapper.title = resourceTitle.value;
syncResources();
});
+ resourceTitle.addEventListener("keyup", function (event) {
+ if (event.key === "Enter") {
+ resourceTitle.blur();
+ }
+ });
resourceTitle.addEventListener("blur", () => {
resourceTitle.style.pointerEvents = "none";
@@ -301,6 +306,7 @@ const stampTool = () =>
const vpc = viewport.canvasToView(x, y);
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+ state.erasePrevCursor && state.erasePrevCursor();
uiCtx.save();
@@ -314,16 +320,7 @@ const stampTool = () =>
}
// Draw current cursor location
- uiCtx.lineWidth = 3;
- uiCtx.strokeStyle = "#FFF";
-
- uiCtx.beginPath();
- uiCtx.moveTo(vpc.x, vpc.y + 10);
- uiCtx.lineTo(vpc.x, vpc.y - 10);
- uiCtx.moveTo(vpc.x + 10, vpc.y);
- uiCtx.lineTo(vpc.x - 10, vpc.y);
- uiCtx.stroke();
-
+ state.erasePrevCursor = _tool._cursor_draw(x, y);
uiCtx.restore();
};