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(); };