diff --git a/css/index.css b/css/index.css index 54843d5..fb1d977 100644 --- a/css/index.css +++ b/css/index.css @@ -104,31 +104,27 @@ body { grid-row-gap: 0px; } -.maskCanvasMonitor .overMaskCanvasMonitor .initImgCanvasMonitor { - position: absolute; -} - /* Mask colors for mask inversion */ /* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */ -.maskPaintCanvas { +.mask-canvas { opacity: 0%; } -.maskPaintCanvas.display { +.mask-canvas.display { opacity: 40%; filter: invert(100%); } -.maskPaintCanvas.display.opaque { +.mask-canvas.display.opaque { opacity: 100%; } -.maskPaintCanvas.display.clear { +.mask-canvas.display.clear { filter: invert(71%) sepia(46%) saturate(6615%) hue-rotate(321deg) brightness(106%) contrast(100%); } -.maskPaintCanvas.display.hold { +.mask-canvas.display.hold { filter: invert(41%) sepia(16%) saturate(5181%) hue-rotate(218deg) brightness(103%) contrast(108%); } @@ -215,11 +211,6 @@ div.prompt-wrapper > textarea { margin: 0; padding: 0; - top: 0px; - bottom: 0px; - left: 0px; - right: 0; - resize: vertical; } @@ -228,7 +219,6 @@ div.prompt-wrapper > textarea:focus { } /* Tool buttons */ - .button-array { display: flex; justify-content: stretch; diff --git a/css/layers.css b/css/layers.css index 2041088..8675b29 100644 --- a/css/layers.css +++ b/css/layers.css @@ -4,3 +4,43 @@ width: 100%; height: 150px; } + +#layer-manager .menu-container { + height: 200px; +} + +.layer-render-target { + position: fixed; + background-color: #466; + + margin: 0; + padding: 0; + + top: 0; + left: 0; + + width: 100%; + height: 100%; +} + +.layer-render-target .collection { + position: absolute; +} + +.layer-render-target .collection > .collection-input-overlay { + position: absolute; + + top: 0; + left: 0; + + z-index: 10; +} + +.layer-render-target canvas { + position: absolute; + + top: 0; + left: 0; + bottom: 0; + right: 0; +} diff --git a/css/ui/toolbar.css b/css/ui/toolbar.css index bb69d01..39c9341 100644 --- a/css/ui/toolbar.css +++ b/css/ui/toolbar.css @@ -9,6 +9,10 @@ background-color: var(--c-primary); } +#ui-toolbar * { + user-select: none; +} + #ui-toolbar .handle { display: flex; align-items: center; diff --git a/index.html b/index.html index ef07ffa..2bc6877 100644 --- a/index.html +++ b/index.html @@ -191,123 +191,27 @@
- -
-
Layer Debug View
- -
- -
-
-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
-
- -
-
-
-
-
- - -

lol ur browser sucks

-
-
- -

lol ur browser sucks

-
-
-
-
-
+
- - - + + + + - - + + + + + @@ -315,6 +219,12 @@ - + + + diff --git a/js/index.js b/js/index.js index bf78890..4727faf 100644 --- a/js/index.js +++ b/js/index.js @@ -48,7 +48,6 @@ var stableDiffusionData = { }; // stuff things use -var blockNewImages = false; var returnedImages; var imageIndex = 0; var tmpImgXYWH = {}; @@ -57,15 +56,6 @@ var url = "/sdapi/v1/"; var endpoint = "txt2img"; var frameX = 512; var frameY = 512; -var prevMouseX = 0; -var prevMouseY = 0; -var mouseX = 0; -var mouseY = 0; -var canvasX = 0; -var canvasY = 0; -var heldButton = 0; -var snapX = 0; -var snapY = 0; var drawThis = {}; const basePixelCount = 64; //64 px - ALWAYS 64 PX var scaleFactor = 8; //x64 px @@ -89,44 +79,6 @@ var stopMarching = null; var inProgress = false; var marchCoords = {}; -// info div, sometimes hidden -let mouseXInfo = document.getElementById("mouseX"); -let mouseYInfo = document.getElementById("mouseY"); -let canvasXInfo = document.getElementById("canvasX"); -let canvasYInfo = document.getElementById("canvasY"); -let snapXInfo = document.getElementById("snapX"); -let snapYInfo = document.getElementById("snapY"); -let heldButtonInfo = document.getElementById("heldButton"); - -// canvases and related -const ovCanvas = document.getElementById("overlayCanvas"); // where mouse cursor renders -const ovCtx = ovCanvas.getContext("2d"); -const tgtCanvas = document.getElementById("targetCanvas"); // where "box" gets drawn before dream happens -const tgtCtx = tgtCanvas.getContext("2d"); -const maskPaintCanvas = document.getElementById("maskPaintCanvas"); // where masking brush gets painted -const maskPaintCtx = maskPaintCanvas.getContext("2d"); -const tempCanvas = document.getElementById("tempCanvas"); // where select/rejects get superimposed temporarily -const tempCtx = tempCanvas.getContext("2d"); -const imgCanvas = document.getElementById("canvas"); // where dreams go -const imgCtx = imgCanvas.getContext("2d"); -const bgCanvas = document.getElementById("backgroundCanvas"); // gray bg grid -const bgCtx = bgCanvas.getContext("2d"); - -// Layering -const imageCollection = layers.registerCollection("image", { - name: "Image Layers", - scope: { - always: { - key: "default", - options: { - name: "Default Image Layer", - }, - }, - }, -}); - -layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true}); - // function startup() { testHostConfiguration(); @@ -163,9 +115,6 @@ function startup() { changeSeed(); changeOverMaskPx(); changeHiResFix(); - document.getElementById("overlayCanvas").onmousemove = mouseMove; - document.getElementById("overlayCanvas").onmousedown = mouseDown; - document.getElementById("overlayCanvas").onmouseup = mouseUp; document.getElementById("scaleFactor").value = scaleFactor; } @@ -197,6 +146,10 @@ function testHostConfiguration() { "Host seems to be invalid! Please fix your host here:", current ); + else + host = current.endsWith("/") + ? current.substring(0, current.length - 1) + : current; } else { requestHost( "This seems to be the first time you are using openOutpaint! Please set your host here:" @@ -205,11 +158,7 @@ function testHostConfiguration() { } function testHostConnection() { - function CheckInProgressError(message = "") { - this.name = "CheckInProgressError"; - this.message = message; - } - CheckInProgressError.prototype = Object.create(Error.prototype); + class CheckInProgressError extends Error {} const connectionIndicator = document.getElementById( "connection-status-indicator" @@ -343,131 +292,9 @@ function testHostConnection() { checkAgain(); } -function dream( - x, - y, - prompt, - extra = { - method: endpoint, - stopMarching: () => {}, - bb: {x, y, w: prompt.width, h: prompt.height}, - } -) { - tmpImgXYWH.x = x; - tmpImgXYWH.y = y; - tmpImgXYWH.w = prompt.width; - tmpImgXYWH.h = prompt.height; - console.log( - "dreaming to " + - host + - url + - (extra.method || endpoint) + - ":\r\n" + - JSON.stringify(prompt) - ); - console.info(`dreaming "${prompt.prompt}"`); - console.debug(prompt); - - // Start checking for progress - const progressCheck = checkProgress(extra.bb); - postData(prompt, extra) - .then((data) => { - returnedImages = data.images; - totalImagesReturned = data.images.length; - blockNewImages = true; - //console.log(data); // JSON data parsed by `data.json()` call - imageAcceptReject(x, y, data, extra); - }) - .finally(() => clearInterval(progressCheck)); -} - -async function postData(promptData, extra = null) { - this.host = document.getElementById("host").value; - // Default options are marked with * - const response = await fetch( - this.host + this.url + extra.method || endpoint, - { - method: "POST", // *GET, POST, PUT, DELETE, etc. - mode: "cors", // no-cors, *cors, same-origin - cache: "default", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - redirect: "follow", // manual, *follow, error - referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - body: JSON.stringify(promptData), // body data type must match "Content-Type" header - } - ); - return response.json(); // parses JSON response into native JavaScript objects -} - -function imageAcceptReject(x, y, data, extra = null) { - inProgress = false; - document.getElementById("progressDiv").remove(); - const img = new Image(); - img.onload = function () { - backupAndClearMask(x, y, img.width, img.height); - tempCtx.drawImage(img, x, y); //imgCtx for actual image, tmp for... holding? - var div = document.createElement("div"); - div.id = "veryTempDiv"; - div.style.position = "absolute"; - div.style.left = parseInt(x) + "px"; - div.style.top = parseInt(y + data.parameters.height) + "px"; - div.style.width = "200px"; - div.style.height = "70px"; - div.innerHTML = - ' of '; - - document.getElementById("tempDiv").appendChild(div); - document.getElementById("currentImgIndex").innerText = "1"; - document.getElementById("totalImgIndex").innerText = totalImagesReturned; - }; - // set the image displayed as the first regardless of batch size/count - imageIndex = 0; - // load the image data after defining the closure - img.src = "data:image/png;base64," + returnedImages[imageIndex]; -} - -function accept(evt) { - // write image to imgcanvas - stopMarching && stopMarching(); - stopMarching = null; - clearBackupMask(); - placeImage(); - removeChoiceButtons(); - clearTargetMask(); - blockNewImages = false; -} - -function reject(evt) { - // remove image entirely - stopMarching && stopMarching(); - stopMarching = null; - restoreBackupMask(); - clearBackupMask(); - clearTargetMask(); - removeChoiceButtons(); - blockNewImages = false; -} - -function resource(evt) { - // send image to resources - const img = new Image(); - // load the image data after defining the closure - img.src = "data:image/png;base64," + returnedImages[imageIndex]; - - tools.stamp.state.addResource( - prompt("Enter new resource name", "Dream Resource"), - img - ); -} - function newImage(evt) { clearPaintedMask(); clearBackupMask(); - clearTargetMask(); commands.runCommand("eraseImage", "Clear Canvas", { x: 0, y: 0, @@ -548,10 +375,6 @@ function clearBackupMask() { backupMaskY = null; } -function clearTargetMask() { - tgtCtx.clearRect(0, 0, tgtCanvas.width, tgtCanvas.height); -} - function clearImgMask() { imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height); } @@ -581,122 +404,37 @@ function sleep(ms) { } function march(bb) { + const expanded = {...bb}; + expanded.x--; + expanded.y--; + expanded.w += 2; + expanded.h += 2; + + // Get temporary layer to draw marching ants + const layer = imageCollection.registerLayer(null, { + bb: expanded, + }); + layer.canvas.style.imageRendering = "pixelated"; let offset = 0; const interval = setInterval(() => { - drawMarchingAnts(bb, offset++); - offset %= 16; + drawMarchingAnts(layer.ctx, bb, offset++); + offset %= 12; }, 20); - return () => clearInterval(interval); + return () => { + clearInterval(interval); + imageCollection.deleteLayer(layer); + }; } -function drawMarchingAnts(bb, offset) { - clearTargetMask(); - tgtCtx.strokeStyle = "#FFFFFFFF"; //"#55000077"; - tgtCtx.setLineDash([4, 2]); - tgtCtx.lineDashOffset = -offset; - tgtCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); -} - -function checkProgress(bb) { - document.getElementById("progressDiv") && - document.getElementById("progressDiv").remove(); - // Skip image to stop using a ton of networking resources - endpoint = "progress?skip_current_image=true"; - var div = document.createElement("div"); - div.id = "progressDiv"; - div.style.position = "absolute"; - div.style.width = "200px"; - div.style.height = "70px"; - div.style.left = parseInt(bb.x + bb.w - 100) + "px"; - div.style.top = parseInt(bb.y + bb.h) + "px"; - div.innerHTML = ''; - document.getElementById("tempDiv").appendChild(div); - return setInterval(() => { - fetch(host + url + endpoint) - .then((response) => response.json()) - .then((data) => { - var estimate = - Math.round(data.progress * 100) + - "% :: " + - Math.floor(data.eta_relative) + - " sec."; - - document.getElementById("estRemaining").innerText = estimate; - }); - }, 1000); -} - -function mouseMove(evt) { - const rect = ovCanvas.getBoundingClientRect(); // not-quite pixel offset was driving me insane - const canvasOffsetX = rect.left; - const canvasOffsetY = rect.top; - heldButton = evt.buttons; - mouseXInfo.innerText = mouseX = evt.clientX; - mouseYInfo.innerText = mouseY = evt.clientY; - canvasXInfo.innerText = canvasX = parseInt(evt.clientX - rect.left); - canvasYInfo.innerText = canvasY = parseInt(evt.clientY - rect.top); - snapXInfo.innerText = canvasX + snap(canvasX); - snapYInfo.innerText = canvasY + snap(canvasY); - heldButtonInfo.innerText = heldButton; - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); // clear out the previous mouse cursor - if (placingArbitraryImage) { - // ugh refactor so this isn't duplicated between arbitrary image and dream reticle modes - snapOffsetX = 0; - snapOffsetY = 0; - if (snapToGrid) { - snapOffsetX = snap(canvasX, false); - snapOffsetY = snap(canvasY, false); - } - finalX = snapOffsetX + canvasX; - finalY = snapOffsetY + canvasY; - ovCtx.drawImage(arbitraryImage, finalX, finalY); - } -} - -function mouseDown(evt) { - const rect = ovCanvas.getBoundingClientRect(); - var oddOffset = 0; - if (scaleFactor % 2 != 0) { - oddOffset = basePixelCount / 2; - } - if (evt.button == 0) { - // left click - if (placingArbitraryImage) { - var nextBox = {}; - nextBox.x = evt.offsetX; - nextBox.y = evt.offsetY; - nextBox.w = arbitraryImageData.width; - nextBox.h = arbitraryImageData.height; - dropTargets.push(nextBox); - } - } -} - -function mouseUp(evt) { - if (evt.button == 0) { - // left click - if (placingArbitraryImage) { - // jeez i REALLY need to refactor tons of this to not be duplicated all over, that's definitely my next chore after figuring out that razza frazza overmask fade - var target = dropTargets[dropTargets.length - 1]; //get the last one... why am i storing all of them? - snapOffsetX = 0; - snapOffsetY = 0; - if (snapToGrid) { - snapOffsetX = snap(target.x, false); - snapOffsetY = snap(target.y, false); - } - finalX = snapOffsetX + target.x; - finalY = snapOffsetY + target.y; - - drawThis.x = finalX; - drawThis.y = finalY; - drawThis.w = target.w; - drawThis.h = target.h; - drawIt = drawThis; // i still think this is really stupid and redundant and unnecessary and redundant - drop(drawIt); - } - } +function drawMarchingAnts(ctx, bb, offset) { + ctx.clearRect(0, 0, bb.w + 2, bb.h + 2); + ctx.strokeStyle = "#FFFFFFFF"; //"#55000077"; + ctx.strokeWidth = "2px"; + ctx.setLineDash([4, 2]); + ctx.lineDashOffset = -offset; + ctx.strokeRect(1, 1, bb.w, bb.h); } function changeSampler() { @@ -815,10 +553,11 @@ function drawBackground() { // Checkerboard let darkTileColor = "#333"; let lightTileColor = "#555"; - for (var x = 0; x < bgCanvas.width; x += 64) { - for (var y = 0; y < bgCanvas.height; y += 64) { - bgCtx.fillStyle = (x + y) % 128 === 0 ? lightTileColor : darkTileColor; - bgCtx.fillRect(x, y, 64, 64); + for (var x = 0; x < bgLayer.canvas.width; x += 64) { + for (var y = 0; y < bgLayer.canvas.height; y += 64) { + bgLayer.ctx.fillStyle = + (x + y) % 128 === 0 ? lightTileColor : darkTileColor; + bgLayer.ctx.fillRect(x, y, 64, 64); } } } @@ -1097,6 +836,18 @@ function loadSettings() { // document.getElementById("overMaskPx").value = Number(_overmask_px); } -document.getElementById("mainHSplit").addEventListener("wheel", (evn) => { - evn.preventDefault(); -}); +imageCollection.element.addEventListener( + "wheel", + (evn) => { + evn.preventDefault(); + }, + {passive: false} +); + +imageCollection.element.addEventListener( + "contextmenu", + (evn) => { + evn.preventDefault(); + }, + {passive: false} +); diff --git a/js/infinity.js b/js/infinity.js index 3c2f696..f1aef79 100644 --- a/js/infinity.js +++ b/js/infinity.js @@ -106,7 +106,7 @@ const infinity = { }, }; -infinity._init(); +//infinity._init(); Array.from(document.getElementsByClassName("display-canvas")).forEach( (canvas) => infinity.registerViewport(canvas) ); diff --git a/js/initalize/debug.populate.js b/js/initalize/debug.populate.js new file mode 100644 index 0000000..20f5452 --- /dev/null +++ b/js/initalize/debug.populate.js @@ -0,0 +1,29 @@ +// info div, sometimes hidden +let mouseXInfo = document.getElementById("mouseX"); +let mouseYInfo = document.getElementById("mouseY"); +let canvasXInfo = document.getElementById("canvasX"); +let canvasYInfo = document.getElementById("canvasY"); +let snapXInfo = document.getElementById("snapX"); +let snapYInfo = document.getElementById("snapY"); +let heldButtonInfo = document.getElementById("heldButton"); + +mouse.listen.window.onmousemove.on((evn) => { + mouseXInfo.textContent = evn.x; + mouseYInfo.textContent = evn.y; +}); + +mouse.listen.world.onmousemove.on((evn) => { + canvasXInfo.textContent = evn.x; + canvasYInfo.textContent = evn.y; + snapXInfo.textContent = snap(evn.x); + snapYInfo.textContent = snap(evn.y); +}); + +/** + * Toggles the debug layer (Just run toggledebug() in the console) + */ +const toggledebug = () => { + const hidden = debugCanvas.style.display === "none"; + if (hidden) debugLayer.unhide(); + else debugLayer.hide(); +}; diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js new file mode 100644 index 0000000..d2280b7 --- /dev/null +++ b/js/initalize/layers.populate.js @@ -0,0 +1,182 @@ +// Layering +const imageCollection = layers.registerCollection( + "image", + {w: 2560, h: 1472}, + { + name: "Image Layers", + } +); + +const bgLayer = imageCollection.registerLayer("bg", { + name: "Background", +}); +const imgLayer = imageCollection.registerLayer("image", { + name: "Image", +}); +const maskPaintLayer = imageCollection.registerLayer("mask", { + name: "Mask Paint", +}); +const ovLayer = imageCollection.registerLayer("overlay", { + name: "Overlay", +}); +const debugLayer = imageCollection.registerLayer("debug", { + name: "Debug Layer", +}); + +const imgCanvas = imgLayer.canvas; // where dreams go +const imgCtx = imgLayer.ctx; + +const maskPaintCanvas = maskPaintLayer.canvas; // where mouse cursor renders +const maskPaintCtx = maskPaintLayer.ctx; + +maskPaintCanvas.classList.add("mask-canvas"); + +const ovCanvas = ovLayer.canvas; // where mouse cursor renders +const ovCtx = ovLayer.ctx; + +const debugCanvas = debugLayer.canvas; // where mouse cursor renders +const debugCtx = debugLayer.ctx; + +debugLayer.hide(); // Hidden by default + +layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true}); + +// Where CSS and javascript magic happens to make the canvas viewport work +/** + * Ended up using a CSS transforms approach due to more flexibility on transformations + * and capability to automagically translate input coordinates to layer space. + */ +mouse.registerContext( + "world", + (evn, ctx) => { + ctx.coords.prev.x = ctx.coords.pos.x; + ctx.coords.prev.y = ctx.coords.pos.y; + ctx.coords.pos.x = evn.layerX; + ctx.coords.pos.y = evn.layerY; + }, + {target: imageCollection.inputElement} +); + +/** + * The global viewport object (may be modularized in the future). All + * coordinates given are of the center of the viewport + * + * cx and cy are the viewport's world coordinates, scaled to zoom level. + * _x and _y are actual coordinates in the DOM space + * + * The transform() function does some transforms and writes them to the + * provided element. + */ +const viewport = { + get cx() { + return this._x * this.zoom; + }, + + set cx(v) { + return (this._x = v / this.zoom); + }, + _x: 0, + get cy() { + return this._y * this.zoom; + }, + set cy(v) { + return (this._y = v / this.zoom); + }, + _y: 0, + zoom: 1, + rotation: 0, + get w() { + return (window.innerWidth * 1) / this.zoom; + }, + get h() { + return (window.innerHeight * 1) / this.zoom; + }, + /** + * Apply transformation + * + * @param {HTMLElement} el Element to apply CSS transform to + */ + transform(el) { + el.style.transformOrigin = `${this.cx}px ${this.cy}px`; + el.style.transform = `scale(${this.zoom}) translate(${-( + this._x - + this.w / 2 + )}px, ${-(this._y - this.h / 2)}px)`; + }, +}; + +let rotation = 0; +let lastTime = performance.now(); + +const onframe = () => { + const nowTime = performance.now(); + const dt = nowTime - lastTime; + rotation += (10 * dt) / 1000.0; + + lastTime = nowTime; + + viewport.transform(imageCollection.element); + + requestAnimationFrame(onframe); +}; + +onframe(); + +viewport.cx = viewport.w / 2; +viewport.cy = viewport.h / 2; + +let worldInit = null; + +imageCollection.element.style.transformOrigin = `${viewport.cx}px ${viewport.cy}px`; +viewport.transform(imageCollection.element); + +mouse.listen.window.onwheel.on((evn) => { + if (evn.evn.ctrlKey) { + const pcx = viewport.cx; + const pcy = viewport.cy; + if (evn.delta < 0) { + viewport.zoom *= 1 + Math.abs(evn.delta * 0.0002); + } else { + viewport.zoom *= 1 - Math.abs(evn.delta * 0.0002); + } + + viewport.cx = pcx; + viewport.cy = pcy; + + 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(); + } +}); + +mouse.listen.window.btn.middle.onpaintstart.on((evn) => { + worldInit = {x: viewport.cx, y: viewport.cy}; +}); + +mouse.listen.window.btn.middle.onpaint.on((evn) => { + if (worldInit) { + viewport.cx = worldInit.x + (evn.ix - evn.x) / viewport.zoom; + viewport.cy = worldInit.y + (evn.iy - evn.y) / viewport.zoom; + + // Limits + viewport.cx = Math.max(Math.min(viewport.cx, imageCollection.size.w), 0); + viewport.cy = Math.max(Math.min(viewport.cy, imageCollection.size.h), 0); + + // Draw Viewport location + } + + 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(); +}); + +mouse.listen.window.btn.middle.onpaintend.on((evn) => { + worldInit = null; +}); diff --git a/js/initalize/toolbar.populate.js b/js/initalize/toolbar.populate.js new file mode 100644 index 0000000..c7052df --- /dev/null +++ b/js/initalize/toolbar.populate.js @@ -0,0 +1,27 @@ +const tools = {}; + +/** + * Dream tool + */ +tools.dream = dreamTool(); +tools.img2img = img2imgTool(); + +/** + * Mask Editing tools + */ +toolbar.addSeparator(); + +/** + * Mask Brush tool + */ +tools.maskbrush = maskBrushTool(); + +/** + * Image Editing tools + */ +toolbar.addSeparator(); + +tools.selecttransform = selectTransformTool(); +tools.stamp = stampTool(); + +toolbar.tools[0].enable(); diff --git a/js/layers.js b/js/layers.js deleted file mode 100644 index 9dff0b0..0000000 --- a/js/layers.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * This is a manager for the many canvas and content layers that compose the application - * - * It manages canvases and their locations and sizes according to current viewport views - */ - -// Errors -class LayerNestedScopesError extends Error { - // For when a scope is created in another scope -} -class LayerNoScopeError extends Error { - // For when an action that requires a scope is attempted - // in a collection with no scope. -} - -const layers = { - collections: makeWriteOnce({}, "layers.collections"), - - // Registers a new collection - registerCollection: (key, options = {}) => { - defaultOpt(options, { - // If collection is visible on the Layer View Toolbar - visible: true, - // Display name for the collection - name: key, - /** - * If layer creates a layer scope - * - * A layer scope is a context where one, and only one layer inside it or its - * subscopes can be active at a time. Nested scopes are not supported. - * It receives an object of type: - * - * { - * // If there must be a selected layer, pass information to create the first - * always: { - * key, - * options - * } - * } - */ - scope: null, - // Parent collection - parent: null, - }); - - // Finds the closest parent with a defined scope - const findScope = (collection = options.parent) => { - if (!collection) return null; - - if (collection.scope) return collection; - return findScope(collection._parent); - }; - - // Path used for logging purposes - const _logpath = options.parent - ? options.parent + "." + key - : "layers.collections." + key; - - // If we have a scope already, we can't add a new scope - if (options.scope && findScope()) - throw new LayerNestedScopesError(`Layer scopes must not be nested`); - - const collection = makeWriteOnce( - { - _parent: options.parent, - _logpath, - _layers: [], - layers: {}, - - name: options.name, - - scope: options.scope, - // Registers a new layer - registerLayer: (key, options = {}) => { - defaultOpt(options, { - // Display name for the layer - name: key, - }); - - // Path used for logging purposes - const _layerlogpath = _logpath + ".layers." + key; - const layer = makeWriteOnce( - { - _logpath: _layerlogpath, - id: guid(), - name: options.name, - - state: new Proxy( - {visible: true}, - { - set(obj, opt, val) { - switch (opt) { - case "visible": - layer.canvas.style.display = val ? "block" : "none"; - break; - } - obj[opt] = val; - }, - } - ), - - // This is where black magic will take place in the future - // A proxy for the canvas object - canvas: new Proxy(document.createElement("canvas"), {}), - - // Activates this layer in the scope - activate: () => { - const scope = findScope(collection); - if (scope) { - scope.active = layer; - console.debug( - `[layers] Layer ${layer._logpath} now active in scope ${scope._logpath}` - ); - } - }, - - // Deactivates this layer in the scope - deactivate: () => { - const scope = findScope(collection); - if (scope && scope.active === layer) scope.active = null; - console.debug(); - }, - }, - _layerlogpath - ); - - // Add to indexers - collection._layers.push(layer); - collection.layers[key] = layer; - - console.info( - `[layers] Layer '${layer.name}' at ${layer._logpath} registered` - ); - return layer; - }, - - // Deletes a layer - deleteLayer: (layer) => { - collection._layers.splice( - collection._layers.findIndex( - (l) => l.id === layer || l.id === layer.id - ), - 1 - ); - if (typeof layer === "object") { - delete collection.layers[layer.id]; - } else if (typeof layer === "string") { - delete collection.layers[layer]; - } - - console.info(`[layers] Layer '${layer}' deleted`); - }, - }, - _logpath - ); - - if (parent) parent[key] = collection; - else layers.collections[key] = collection; - - console.info( - `[layers] Collection '${options.name}' at ${_logpath} registered` - ); - - // If always, we must create a layer to select - if (options.scope && options.scope.always) - collection - .registerLayer(options.scope.always.key, options.scope.always.options) - .activate(); - - return collection; - }, -}; diff --git a/js/commands.d.js b/js/lib/commands.d.js similarity index 100% rename from js/commands.d.js rename to js/lib/commands.d.js diff --git a/js/commands.js b/js/lib/commands.js similarity index 100% rename from js/commands.js rename to js/lib/commands.js diff --git a/js/input.d.js b/js/lib/input.d.js similarity index 93% rename from js/input.d.js rename to js/lib/input.d.js index 498bd8f..4c5e420 100644 --- a/js/input.d.js +++ b/js/lib/input.d.js @@ -44,7 +44,7 @@ * @typedef MouseListenerContext * @property {Observer} onmousemove A mouse move handler * @property {Observer} onwheel A mouse wheel handler - * @property {MouseListenerBtnContext} btn Button handlers + * @property {Record} btn Button handlers */ /** @@ -65,7 +65,10 @@ * @property {string} id A unique identifier * @property {string} name The key name * @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 {MouseCoordContext} coords Coordinates object + * @property {MouseListenerContext} listen Listeners object */ /** diff --git a/js/input.js b/js/lib/input.js similarity index 64% rename from js/input.js rename to js/lib/input.js index 8e1b15b..5c9608b 100644 --- a/js/input.js +++ b/js/lib/input.js @@ -64,6 +64,7 @@ const mouse = { * @param {object} options Extra options * @param {HTMLElement} [options.target=null] Target filtering * @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 = {}) => { @@ -71,6 +72,7 @@ const mouse = { defaultOpt(options, { target: null, buttons: {0: "left", 1: "middle", 2: "right"}, + genericcb: null, }); // Context information @@ -79,6 +81,7 @@ const mouse = { id: guid(), name, onmove, + onany: options.genericcb, target: options.target, buttons: options.buttons, }; @@ -128,101 +131,69 @@ const mouse = { const _double_click_timeout = {}; const _drag_start_timeout = {}; -window.onmousedown = (evn) => { - const time = performance.now(); +window.addEventListener( + "mousedown", + (evn) => { + const time = performance.now(); - if (_double_click_timeout[evn.button]) { - // ondclick event - mouse._contexts.forEach(({target, name, buttons}) => { - if ((!target || target === evn.target) && buttons[evn.button]) - mouse.listen[name].btn[buttons[evn.button]].ondclick.emit({ - target: evn.target, - buttonId: evn.button, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: time, - }); - }); - } else { - // Start timer - _double_click_timeout[evn.button] = setTimeout( - () => delete _double_click_timeout[evn.button], - inputConfig.dClickTiming - ); - } - - // Set drag start timeout - _drag_start_timeout[evn.button] = setTimeout(() => { - mouse._contexts.forEach(({target, name, buttons}) => { - const key = buttons[evn.button]; - if ( - (!target || target === evn.target) && - !mouse.coords[name].dragging[key].drag && - key - ) { - mouse.listen[name].btn[key].ondragstart.emit({ - target: evn.target, - buttonId: evn.button, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: time, - }); - - mouse.coords[name].dragging[key].drag = true; - } - }); - delete _drag_start_timeout[evn.button]; - }, inputConfig.clickTiming); - - mouse.buttons[evn.button] = time; - - mouse._contexts.forEach(({target, name, buttons}) => { - const key = buttons[evn.button]; - if ((!target || target === evn.target) && key) { - mouse.coords[name].dragging[key] = {}; - mouse.coords[name].dragging[key].target = evn.target; - Object.assign(mouse.coords[name].dragging[key], mouse.coords[name].pos); - - // onpaintstart event - mouse.listen[name].btn[key].onpaintstart.emit({ - target: evn.target, - buttonId: evn.button, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: performance.now(), + if (_double_click_timeout[evn.button]) { + // ondclick event + mouse._contexts.forEach(({target, name, buttons}) => { + if ((!target || target === evn.target) && buttons[evn.button]) + mouse.listen[name].btn[buttons[evn.button]].ondclick.emit({ + target: evn.target, + buttonId: evn.button, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: time, + }); }); + } else { + // Start timer + _double_click_timeout[evn.button] = setTimeout( + () => delete _double_click_timeout[evn.button], + inputConfig.dClickTiming + ); } - }); -}; -window.onmouseup = (evn) => { - const time = performance.now(); + // Set drag start timeout + _drag_start_timeout[evn.button] = setTimeout(() => { + mouse._contexts.forEach(({target, name, buttons}) => { + const key = buttons[evn.button]; + if ( + (!target || target === evn.target) && + !mouse.coords[name].dragging[key].drag && + key + ) { + mouse.listen[name].btn[key].ondragstart.emit({ + target: evn.target, + buttonId: evn.button, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: time, + }); - mouse._contexts.forEach(({target, name, buttons}) => { - const key = buttons[evn.button]; - if ( - (!target || target === evn.target) && - key && - mouse.coords[name].dragging[key] - ) { - const start = { - x: mouse.coords[name].dragging[key].x, - y: mouse.coords[name].dragging[key].y, - }; + mouse.coords[name].dragging[key].drag = true; + } + }); + delete _drag_start_timeout[evn.button]; + }, inputConfig.clickTiming); - // onclick event - const dx = mouse.coords[name].pos.x - start.x; - const dy = mouse.coords[name].pos.y - start.y; + mouse.buttons[evn.button] = time; - if ( - mouse.buttons[evn.button] && - time - mouse.buttons[evn.button] < inputConfig.clickTiming && - dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius - ) - mouse.listen[name].btn[key].onclick.emit({ + mouse._contexts.forEach(({target, name, buttons, onany}) => { + const key = buttons[evn.button]; + if ((!target || target === evn.target) && key) { + onany && onany(); + + mouse.coords[name].dragging[key] = {}; + mouse.coords[name].dragging[key].target = evn.target; + Object.assign(mouse.coords[name].dragging[key], mouse.coords[name].pos); + + // onpaintstart event + mouse.listen[name].btn[key].onpaintstart.emit({ target: evn.target, buttonId: evn.button, x: mouse.coords[name].pos.x, @@ -230,23 +201,52 @@ window.onmouseup = (evn) => { evn, timestamp: performance.now(), }); + } + }); + }, + { + passive: false, + } +); - // onpaintend event - mouse.listen[name].btn[key].onpaintend.emit({ - target: evn.target, - initialTarget: mouse.coords[name].dragging[key].target, - buttonId: evn.button, - ix: mouse.coords[name].dragging[key].x, - iy: mouse.coords[name].dragging[key].y, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: performance.now(), - }); +window.addEventListener( + "mouseup", + (evn) => { + const time = performance.now(); - // ondragend event - if (mouse.coords[name].dragging[key].drag) - mouse.listen[name].btn[key].ondragend.emit({ + mouse._contexts.forEach(({target, name, buttons, onany}) => { + const key = buttons[evn.button]; + if ( + (!target || target === evn.target) && + key && + mouse.coords[name].dragging[key] + ) { + onany && onany(); + const start = { + x: mouse.coords[name].dragging[key].x, + y: mouse.coords[name].dragging[key].y, + }; + + // onclick event + const dx = mouse.coords[name].pos.x - start.x; + const dy = mouse.coords[name].pos.y - start.y; + + if ( + mouse.buttons[evn.button] && + time - mouse.buttons[evn.button] < inputConfig.clickTiming && + dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius + ) + mouse.listen[name].btn[key].onclick.emit({ + target: evn.target, + buttonId: evn.button, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: performance.now(), + }); + + // onpaintend event + mouse.listen[name].btn[key].onpaintend.emit({ target: evn.target, initialTarget: mouse.coords[name].dragging[key].target, buttonId: evn.button, @@ -258,102 +258,122 @@ window.onmouseup = (evn) => { timestamp: performance.now(), }); - mouse.coords[name].dragging[key] = null; + // ondragend event + if (mouse.coords[name].dragging[key].drag) + mouse.listen[name].btn[key].ondragend.emit({ + target: evn.target, + initialTarget: mouse.coords[name].dragging[key].target, + buttonId: evn.button, + ix: mouse.coords[name].dragging[key].x, + iy: mouse.coords[name].dragging[key].y, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: performance.now(), + }); + + mouse.coords[name].dragging[key] = null; + } + }); + + if (_drag_start_timeout[evn.button] !== undefined) { + clearTimeout(_drag_start_timeout[evn.button]); + delete _drag_start_timeout[evn.button]; } - }); + mouse.buttons[evn.button] = null; + }, + {passive: false} +); - if (_drag_start_timeout[evn.button] !== undefined) { - clearTimeout(_drag_start_timeout[evn.button]); - delete _drag_start_timeout[evn.button]; - } - mouse.buttons[evn.button] = null; -}; +window.addEventListener( + "mousemove", + (evn) => { + mouse._contexts.forEach((context) => { + const target = context.target; + const name = context.name; -window.onmousemove = (evn) => { - mouse._contexts.forEach((context) => { - const target = context.target; - const name = context.name; + if (!target || target === evn.target) { + context.onmove(evn, context); - if (!target || target === evn.target) { - context.onmove(evn, context); + mouse.listen[name].onmousemove.emit({ + target: evn.target, + px: mouse.coords[name].prev.x, + py: mouse.coords[name].prev.y, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: performance.now(), + }); - mouse.listen[name].onmousemove.emit({ - target: evn.target, - px: mouse.coords[name].prev.x, - py: mouse.coords[name].prev.y, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: performance.now(), - }); + Object.keys(context.buttons).forEach((index) => { + const key = context.buttons[index]; + // ondragstart event (2) + if (mouse.coords[name].dragging[key]) { + const dx = + mouse.coords[name].pos.x - mouse.coords[name].dragging[key].x; + const dy = + mouse.coords[name].pos.y - mouse.coords[name].dragging[key].y; + if ( + !mouse.coords[name].dragging[key].drag && + dx * dx + dy * dy >= + inputConfig.clickRadius * inputConfig.clickRadius + ) { + mouse.listen[name].btn[key].ondragstart.emit({ + target: evn.target, + buttonId: evn.button, + ix: mouse.coords[name].dragging[key].x, + iy: mouse.coords[name].dragging[key].y, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: performance.now(), + }); - Object.keys(context.buttons).forEach((index) => { - const key = context.buttons[index]; - // ondragstart event (2) - if (mouse.coords[name].dragging[key]) { - const dx = - mouse.coords[name].pos.x - mouse.coords[name].dragging[key].x; - const dy = - mouse.coords[name].pos.y - mouse.coords[name].dragging[key].y; + mouse.coords[name].dragging[key].drag = true; + } + } + + // ondrag event if ( - !mouse.coords[name].dragging[key].drag && - dx * dx + dy * dy >= - inputConfig.clickRadius * inputConfig.clickRadius - ) { - mouse.listen[name].btn[key].ondragstart.emit({ + mouse.coords[name].dragging[key] && + mouse.coords[name].dragging[key].drag + ) + mouse.listen[name].btn[key].ondrag.emit({ target: evn.target, - buttonId: evn.button, + initialTarget: mouse.coords[name].dragging[key].target, + button: index, ix: mouse.coords[name].dragging[key].x, iy: mouse.coords[name].dragging[key].y, + px: mouse.coords[name].prev.x, + py: mouse.coords[name].prev.y, x: mouse.coords[name].pos.x, y: mouse.coords[name].pos.y, evn, timestamp: performance.now(), }); - mouse.coords[name].dragging[key].drag = true; + // onpaint event + if (mouse.coords[name].dragging[key]) { + mouse.listen[name].btn[key].onpaint.emit({ + target: evn.target, + initialTarget: mouse.coords[name].dragging[key].target, + button: index, + ix: mouse.coords[name].dragging[key].x, + iy: mouse.coords[name].dragging[key].y, + px: mouse.coords[name].prev.x, + py: mouse.coords[name].prev.y, + x: mouse.coords[name].pos.x, + y: mouse.coords[name].pos.y, + evn, + timestamp: performance.now(), + }); } - } - - // ondrag event - if ( - mouse.coords[name].dragging[key] && - mouse.coords[name].dragging[key].drag - ) - mouse.listen[name].btn[key].ondrag.emit({ - target: evn.target, - initialTarget: mouse.coords[name].dragging[key].target, - button: index, - ix: mouse.coords[name].dragging[key].x, - iy: mouse.coords[name].dragging[key].y, - px: mouse.coords[name].prev.x, - py: mouse.coords[name].prev.y, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: performance.now(), - }); - - // onpaint event - if (mouse.coords[name].dragging[key]) { - mouse.listen[name].btn[key].onpaint.emit({ - target: evn.target, - initialTarget: mouse.coords[name].dragging[key].target, - button: index, - ix: mouse.coords[name].dragging[key].x, - iy: mouse.coords[name].dragging[key].y, - px: mouse.coords[name].prev.x, - py: mouse.coords[name].prev.y, - x: mouse.coords[name].pos.x, - y: mouse.coords[name].pos.y, - evn, - timestamp: performance.now(), - }); - } - }); - } - }); -}; + }); + } + }); + }, + {passive: false} +); window.addEventListener( "wheel", @@ -382,17 +402,6 @@ mouse.registerContext("window", (evn, ctx) => { ctx.coords.pos.x = evn.clientX; ctx.coords.pos.y = evn.clientY; }); - -mouse.registerContext( - "canvas", - (evn, ctx) => { - ctx.coords.prev.x = ctx.coords.pos.x; - ctx.coords.prev.y = ctx.coords.pos.y; - ctx.coords.pos.x = evn.layerX; - ctx.coords.pos.y = evn.layerY; - }, - document.getElementById("overlayCanvas") -); /** * Keyboard input processing */ diff --git a/js/lib/layers.js b/js/lib/layers.js new file mode 100644 index 0000000..6e38c9b --- /dev/null +++ b/js/lib/layers.js @@ -0,0 +1,264 @@ +/** + * This is a manager for the many canvas and content layers that compose the application + * + * It manages canvases and their locations and sizes according to current viewport views + */ +const layers = { + _collections: [], + collections: makeWriteOnce({}, "layers.collections"), + + listen: { + oncollectioncreate: new Observer(), + oncollectiondelete: new Observer(), + + onlayercreate: new Observer(), + onlayerdelete: new Observer(), + }, + + // Registers a new collection + // Layer collections are a group of layers (canvases) that are rendered in tandem. (same width, height, position, transform, etc) + registerCollection: (key, size, options = {}) => { + defaultOpt(options, { + // Display name for the collection + name: key, + + // Initial layer + initLayer: { + key: "default", + options: {}, + }, + + // Target + targetElement: document.getElementById("layer-render"), + + // Resolution of the image + resolution: size, + }); + + // Path used for logging purposes + const _logpath = "layers.collections." + key; + + // Collection ID + const id = guid(); + + // Collection element + const element = document.createElement("div"); + element.id = `collection-${id}`; + element.style.width = `${size.w}px`; + element.style.height = `${size.h}px`; + element.classList.add("collection"); + + // Input element (overlay element for input handling) + const inputel = document.createElement("div"); + inputel.id = `collection-input-${id}`; + inputel.style.width = `${size.w}px`; + inputel.style.height = `${size.h}px`; + inputel.addEventListener("mouseover", (evn) => { + document.activeElement.blur(); + }); + inputel.classList.add("collection-input-overlay"); + element.appendChild(inputel); + + options.targetElement.appendChild(element); + + const collection = makeWriteOnce( + { + id, + + _logpath, + + _layers: [], + layers: {}, + + name: options.name, + element, + inputElement: inputel, + + size, + resolution: options.resolution, + + active: null, + + /** + * Registers a new layer + * + * @param {string | null} key Name and key to use to access layer. If null, it is a temporary layer. + * @param {object} options + * @param {string} options.name + * @param {?BoundingBox} options.bb + * @param {{w: number, h: number}} options.resolution + * @param {object} options.after + * @returns + */ + registerLayer: (key = null, options = {}) => { + // Make ID + const id = guid(); + + defaultOpt(options, { + // Display name for the layer + name: key || `Temporary ${id}`, + + // Bounding box for layer + bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h}, + + // Bounding box for layer + resolution: null, + + // If set, will insert the layer after the given one + after: null, + }); + + // Calculate resolution + if (!options.resolution) + options.resolution = { + w: (collection.resolution.w / collection.size.w) * options.bb.w, + h: (collection.resolution.h / collection.size.h) * options.bb.h, + }; + + // This layer's canvas + // This is where black magic will take place in the future + /** + * @todo Use the canvas black arts to auto-scale canvas + */ + const canvas = document.createElement("canvas"); + canvas.id = `layer-${id}`; + + canvas.style.left = `${options.bb.x}px`; + canvas.style.top = `${options.bb.y}px`; + canvas.style.width = `${options.bb.w}px`; + canvas.style.height = `${options.bb.h}px`; + canvas.width = options.resolution.w; + canvas.height = options.resolution.h; + + if (!options.after) collection.element.appendChild(canvas); + else { + options.after.canvas.after(canvas); + } + + const ctx = canvas.getContext("2d"); + + // Path used for logging purposes + const _layerlogpath = key + ? _logpath + ".layers." + key + : _logpath + ".layers[" + id + "]"; + const layer = makeWriteOnce( + { + _logpath: _layerlogpath, + _collection: collection, + + id, + key, + name: options.name, + + state: new Proxy( + {visible: true}, + { + set(obj, opt, val) { + switch (opt) { + case "visible": + layer.canvas.style.display = val ? "block" : "none"; + break; + } + obj[opt] = val; + }, + } + ), + + /** Our canvas */ + canvas, + ctx, + + /** + * 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 + */ + moveTo(x, y) { + canvas.style.left = `${x}px`; + canvas.style.top = `${y}px`; + }, + + // Hides this layer (don't draw) + hide() { + this.canvas.style.display = "none"; + }, + // Hides this layer (don't draw) + unhide() { + this.canvas.style.display = "block"; + }, + + // Activates this layer + activate() { + collection.active = this; + }, + }, + _layerlogpath, + ["active"] + ); + + // Add to indexers + if (!options.after) collection._layers.push(layer); + else { + const index = collection._layers.findIndex( + (l) => l === options.after + ); + collection._layers.splice(index, 0, layer); + } + if (key) collection.layers[key] = layer; + + if (key === null) + console.debug( + `[layers] Anonymous layer '${layer.name}' registered` + ); + else + console.info( + `[layers] Layer '${layer.name}' at ${layer._logpath} registered` + ); + + layers.listen.onlayercreate.emit({ + layer, + }); + return layer; + }, + + // Deletes a layer + deleteLayer: (layer) => { + const lobj = collection._layers.splice( + collection._layers.findIndex( + (l) => l.id === layer || l.id === layer.id + ), + 1 + )[0]; + if (!lobj) return; + + layers.listen.onlayerdelete.emit({ + layer: lobj, + }); + if (lobj.key) delete collection.layers[lobj.key]; + + collection.element.removeChild(lobj.canvas); + + if (lobj.key) console.info(`[layers] Layer '${lobj.key}' deleted`); + else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`); + }, + }, + _logpath, + ["active"] + ); + + layers._collections.push(collection); + layers.collections[key] = collection; + + console.info( + `[layers] Collection '${options.name}' at ${_logpath} registered` + ); + + // We must create a layer to select + collection + .registerLayer(options.initLayer.key, options.initLayer.options) + .activate(); + + return collection; + }, +}; diff --git a/js/ui/toolbar.js b/js/lib/toolbar.js similarity index 84% rename from js/ui/toolbar.js rename to js/lib/toolbar.js index 955f6df..54a17c5 100644 --- a/js/ui/toolbar.js +++ b/js/lib/toolbar.js @@ -177,46 +177,21 @@ const _toolbar_input = { * Dream and img2img tools */ const _reticle_draw = (evn, snapToGrid = true) => { - if (evn.target.id === "overlayCanvas") { - const bb = getBoundingBox( - evn.x, - evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, - snapToGrid && basePixelCount - ); + 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 - } + // 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 + // TEMP + ovCtx.fillStyle = "#0FF"; + ovCtx.beginPath(); + ovCtx.arc(evn.x, evn.y, 5, 0, Math.PI * 2); + ovCtx.fill(); }; - -const tools = {}; - -/** - * Dream tool - */ -tools.dream = dreamTool(); -tools.img2img = img2imgTool(); - -/** - * Mask Editing tools - */ -toolbar.addSeparator(); - -/** - * Mask Brush tool - */ -tools.maskbrush = maskBrushTool(); - -/** - * Image Editing tools - */ -toolbar.addSeparator(); - -tools.selecttransform = selectTransformTool(); -tools.stamp = stampTool(); - -toolbar.tools[0].enable(); diff --git a/js/util.js b/js/lib/util.js similarity index 100% rename from js/util.js rename to js/lib/util.js diff --git a/js/settingsbar.js b/js/settingsbar.js index f7848cd..a88fb30 100644 --- a/js/settingsbar.js +++ b/js/settingsbar.js @@ -150,7 +150,7 @@ function createSlider(name, wrapper, options = {}) { }); mouse.listen.window.btn.left.ondrag.on((evn) => { - if (evn.target === overEl) { + if (evn.initialTarget === overEl) { setValue( Math.max( options.min, diff --git a/js/ui/explore.js b/js/ui/explore.js new file mode 100644 index 0000000..212a275 --- /dev/null +++ b/js/ui/explore.js @@ -0,0 +1,3 @@ +/** + * This is a simple implementation of layer interaction + */ diff --git a/js/ui/history.js b/js/ui/floating/history.js similarity index 100% rename from js/ui/history.js rename to js/ui/floating/history.js diff --git a/js/ui/tool/dream.d.js b/js/ui/tool/dream.d.js new file mode 100644 index 0000000..24da203 --- /dev/null +++ b/js/ui/tool/dream.d.js @@ -0,0 +1,23 @@ +/** + * Stable Diffusion Request + * + * @typedef StableDiffusionRequest + * @property {string} prompt Stable Diffusion prompt + * @property {string} negative_prompt Stable Diffusion negative prompt + */ + +/** + * Stable Diffusion Response + * + * @typedef StableDiffusionResponse + * @property {string[]} images Response images + */ + +/** + * Stable Diffusion Progress Response + * + * @typedef StableDiffusionProgressResponse + * @property {number} progress Progress (from 0 to 1) + * @property {number} eta_relative Estimated finish time + * @property {?string} current_image Progress image + */ diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 3eb5a06..d69c200 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -1,5 +1,256 @@ -const dream_generate_callback = (evn, state) => { - if (evn.target.id === "overlayCanvas" && !blockNewImages) { +let blockNewImages = false; + +/** + * Starts progress monitoring bar + * + * @param {BoundingBox} bb Bouding Box to draw progress to + * @returns {() => void} + */ +const _monitorProgress = (bb) => { + const minDelay = 1000; + + const apiURL = `${host}${url}progress?skip_current_image=true`; + + const expanded = {...bb}; + expanded.x--; + expanded.y--; + expanded.w += 2; + expanded.h += 2; + + // Get temporary layer to draw progress bar + const layer = imageCollection.registerLayer(null, { + bb: expanded, + }); + layer.canvas.style.opacity = "70%"; + + let running = true; + + const _checkProgress = async () => { + const init = performance.now(); + + try { + const response = await fetch(apiURL); + /** @type {StableDiffusionProgressResponse} */ + const data = await response.json(); + + // Draw Progress Bar + layer.ctx.fillStyle = "#5F5"; + layer.ctx.fillRect(1, 1, bb.w * data.progress, 10); + + // Draw Progress Text + layer.ctx.clearRect(0, 11, expanded.w, 40); + layer.ctx.fillStyle = "#FFF"; + + layer.ctx.fillRect(0, 15, 60, 25); + layer.ctx.fillRect(bb.w - 58, 15, 60, 25); + + layer.ctx.font = "20px Open Sans"; + layer.ctx.fillStyle = "#000"; + layer.ctx.textAlign = "right"; + layer.ctx.fillText(`${Math.round(data.progress * 100)}%`, 55, 35); + + // Draw ETA Text + layer.ctx.fillText(`${Math.round(data.eta_relative)}s`, bb.w - 5, 35); + } finally { + } + + const timeSpent = performance.now() - init; + setTimeout(() => { + if (running) _checkProgress(); + }, Math.max(0, minDelay - timeSpent)); + }; + + _checkProgress(); + + return () => { + imageCollection.deleteLayer(layer); + running = false; + }; +}; + +/** + * Starts a dream + * + * @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to + * @param {StableDiffusionRequest} request Stable diffusion request + * @returns {Promise} + */ +const _dream = async (endpoint, request) => { + const apiURL = `${host}${url}${endpoint}`; + + /** @type {StableDiffusionResponse} */ + let data = null; + try { + const response = await fetch(apiURL, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }); + + data = await response.json(); + } finally { + } + + return data.images; +}; + +/** + * Generate and pick an image for placement + * + * @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to + * @param {StableDiffusionRequest} request Stable diffusion request + * @param {BoundingBox} bb Generated image placement location + * @returns {Promise} + */ +const _generate = async (endpoint, request, bb) => { + const requestCopy = {...request}; + + // Images to select through + let at = 0; + /** @type {Image[]} */ + const images = []; + /** @type {HTMLDivElement} */ + let imageSelectMenu = null; + + // Layer for the images + const layer = imageCollection.registerLayer(null, { + bb, + after: imgLayer, + }); + + const redraw = () => { + const image = new Image(); + 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, 0, 0); + }); + }; + + const stopMarchingAnts = march(bb); + + // First Dream Run + let stopProgress = _monitorProgress(bb); + images.push(...(await _dream(endpoint, requestCopy))); + stopProgress(); + + // Cleans up + const clean = () => { + stopMarchingAnts(); + imageCollection.inputElement.removeChild(imageSelectMenu); + imageCollection.deleteLayer(layer); + blockNewImages = false; + }; + + const makeElement = (type, x, y) => { + const el = document.createElement(type); + el.style.position = "absolute"; + el.style.left = `${x}px`; + el.style.top = `${y}px`; + + // We can use the input element to add interactible html elements in the world + imageCollection.inputElement.appendChild(el); + + return el; + }; + + redraw(); + + imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h); + + const imageindextxt = document.createElement("button"); + imageindextxt.textContent = `${at + 1}/${images.length}`; + imageindextxt.addEventListener("click", () => { + at = 0; + + imageindextxt.textContent = `${at + 1}/${images.length}`; + redraw(); + }); + + const backbtn = document.createElement("button"); + backbtn.textContent = "<"; + backbtn.title = "Previous Image"; + backbtn.addEventListener("click", () => { + at--; + if (at < 0) at = images.length - 1; + + imageindextxt.textContent = `${at + 1}/${images.length}`; + redraw(); + }); + imageSelectMenu.appendChild(backbtn); + imageSelectMenu.appendChild(imageindextxt); + + const nextbtn = document.createElement("button"); + nextbtn.textContent = ">"; + nextbtn.title = "Next Image"; + nextbtn.addEventListener("click", () => { + at++; + if (at >= images.length) at = 0; + + imageindextxt.textContent = `${at + 1}/${images.length}`; + redraw(); + }); + imageSelectMenu.appendChild(nextbtn); + + const morebtn = document.createElement("button"); + morebtn.textContent = "+"; + morebtn.title = "Generate More"; + morebtn.addEventListener("click", async () => { + let stopProgress = _monitorProgress(bb); + images.push(...(await _dream(endpoint, requestCopy))); + stopProgress(); + + imageindextxt.textContent = `${at + 1}/${images.length}`; + }); + imageSelectMenu.appendChild(morebtn); + + const acceptbtn = document.createElement("button"); + acceptbtn.textContent = "Y"; + acceptbtn.title = "Apply Current"; + acceptbtn.addEventListener("click", async () => { + commands.runCommand("drawImage", "Image Dream", { + x: bb.x, + y: bb.y, + image: layer.canvas, + }); + clean(); + }); + imageSelectMenu.appendChild(acceptbtn); + + const discardbtn = document.createElement("button"); + discardbtn.textContent = "N"; + discardbtn.title = "Cancel"; + discardbtn.addEventListener("click", async () => { + clean(); + }); + imageSelectMenu.appendChild(discardbtn); + + const resourcebtn = document.createElement("button"); + resourcebtn.textContent = "R"; + resourcebtn.title = "Save to Resources"; + resourcebtn.addEventListener("click", async () => { + const img = new Image(); + // load the image data after defining the closure + img.src = "data:image/png;base64," + images[at]; + img.addEventListener("load", () => { + const response = prompt("Enter new resource name", "Dream Resource"); + if (response) tools.stamp.state.addResource(response, img); + }); + }); + imageSelectMenu.appendChild(resourcebtn); +}; + +/** + * Callback for generating a image (dream tool) + * + * @param {*} evn + * @param {*} state + */ +const dream_generate_callback = async (evn, state) => { + if (!blockNewImages) { const bb = getBoundingBox( evn.x, evn.y, @@ -19,9 +270,6 @@ const dream_generate_callback = (evn, state) => { // Don't allow another image until is finished blockNewImages = true; - // Setup marching ants - stopMarching = march(bb); - // Setup some basic information for SD request.width = bb.w; request.height = bb.h; @@ -32,7 +280,7 @@ const dream_generate_callback = (evn, state) => { // Use txt2img if canvas is blank if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) { // Dream - dream(bb.x, bb.y, request, {method: "txt2img", stopMarching, bb}); + _generate("txt2img", request, bb); } else { // Use img2img if not @@ -101,7 +349,7 @@ const dream_generate_callback = (evn, state) => { auxCtx.fillRect(0, 0, bb.w, bb.h); request.mask = auxCanvas.toDataURL(); // Dream - dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb}); + _generate("img2img", request, bb); } } }; @@ -148,7 +396,7 @@ function applyOvermask(canvas, ctx, px) { * Image to Image */ const dream_img2img_callback = (evn, state) => { - if (evn.target.id === "overlayCanvas" && !blockNewImages) { + if (!blockNewImages) { const bb = getBoundingBox( evn.x, evn.y, @@ -233,7 +481,7 @@ const dream_img2img_callback = (evn, state) => { request.inpaint_full_res = state.fullResolution; // Dream - dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb}); + _generate("img2img", request, bb); } }; @@ -248,23 +496,22 @@ const dreamTool = () => // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); state.mousemovecb({ - ...mouse.coords.canvas.pos, - target: {id: "overlayCanvas"}, + ...mouse.coords.world.pos, }); // Start Listeners - mouse.listen.canvas.onmousemove.on(state.mousemovecb); - mouse.listen.canvas.btn.left.onclick.on(state.dreamcb); - mouse.listen.canvas.btn.right.onclick.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); + mouse.listen.world.btn.right.onclick.on(state.erasecb); // Display Mask setMask(state.invertMask ? "hold" : "clear"); }, (state, opt) => { // Clear Listeners - mouse.listen.canvas.onmousemove.clear(state.mousemovecb); - mouse.listen.canvas.btn.left.onclick.clear(state.dreamcb); - mouse.listen.canvas.btn.right.onclick.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.mousemovecb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); + mouse.listen.world.btn.right.onclick.clear(state.erasecb); // Hide Mask setMask("none"); @@ -274,7 +521,10 @@ const dreamTool = () => state.snapToGrid = true; state.invertMask = false; state.overMaskPx = 0; - state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid); + state.mousemovecb = (evn) => { + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + _reticle_draw(evn, state.snapToGrid); + }; state.dreamcb = (evn) => { dream_generate_callback(evn, state); }; @@ -330,23 +580,22 @@ const img2imgTool = () => // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); state.mousemovecb({ - ...mouse.coords.canvas.pos, - target: {id: "overlayCanvas"}, + ...mouse.coords.world.pos, }); // Start Listeners - mouse.listen.canvas.onmousemove.on(state.mousemovecb); - mouse.listen.canvas.btn.left.onclick.on(state.dreamcb); - mouse.listen.canvas.btn.right.onclick.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); + mouse.listen.world.btn.right.onclick.on(state.erasecb); // Display Mask setMask(state.invertMask ? "hold" : "clear"); }, (state, opt) => { // Clear Listeners - mouse.listen.canvas.onmousemove.clear(state.mousemovecb); - mouse.listen.canvas.btn.left.onclick.clear(state.dreamcb); - mouse.listen.canvas.btn.right.onclick.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.mousemovecb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); + mouse.listen.world.btn.right.onclick.clear(state.erasecb); // Hide mask setMask("none"); @@ -362,45 +611,45 @@ const img2imgTool = () => state.keepBorderSize = 64; state.mousemovecb = (evn) => { + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); _reticle_draw(evn, state.snapToGrid); - if (evn.target.id === "overlayCanvas") { - const bb = getBoundingBox( - evn.x, - evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, - state.snapToGrid && basePixelCount + const bb = getBoundingBox( + evn.x, + evn.y, + basePixelCount * scaleFactor, + basePixelCount * scaleFactor, + state.snapToGrid && basePixelCount + ); + + // For displaying border mask + const auxCanvas = document.createElement("canvas"); + auxCanvas.width = bb.w; + auxCanvas.height = bb.h; + 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.fillRect( + bb.w - state.keepBorderSize, + 0, + state.keepBorderSize, + bb.h ); - - // For displaying border mask - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = bb.w; - auxCanvas.height = bb.h; - 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.fillRect( - bb.w - state.keepBorderSize, - 0, - state.keepBorderSize, - bb.h - ); - auxCtx.fillRect( - 0, - bb.h - state.keepBorderSize, - bb.w, - state.keepBorderSize - ); - } - - const tmp = ovCtx.globalAlpha; - ovCtx.globalAlpha = 0.4; - ovCtx.drawImage(auxCanvas, bb.x, bb.y); - ovCtx.globalAlpha = tmp; + auxCtx.fillRect( + 0, + bb.h - state.keepBorderSize, + bb.w, + state.keepBorderSize + ); + console.debug("hey"); } + + const tmp = ovCtx.globalAlpha; + ovCtx.globalAlpha = 0.4; + ovCtx.drawImage(auxCanvas, bb.x, bb.y); + ovCtx.globalAlpha = tmp; }; state.dreamcb = (evn) => { dream_img2img_callback(evn, state); diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index bc4af36..38a926c 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -1,5 +1,5 @@ const setMask = (state) => { - const canvas = document.querySelector("#maskPaintCanvas"); + const canvas = imageCollection.layers.mask.canvas; switch (state) { case "clear": canvas.classList.remove("hold"); @@ -23,43 +23,33 @@ const setMask = (state) => { }; const _mask_brush_draw_callback = (evn, state) => { - if ( - (evn.initialTarget && evn.initialTarget.id === "overlayCanvas") || - (!evn.initialTarget && evn.target.id === "overlayCanvas") - ) { - maskPaintCtx.globalCompositeOperation = "source-over"; - maskPaintCtx.strokeStyle = "black"; + maskPaintCtx.globalCompositeOperation = "source-over"; + maskPaintCtx.strokeStyle = "black"; - maskPaintCtx.lineWidth = state.brushSize; - maskPaintCtx.beginPath(); - maskPaintCtx.moveTo( - evn.px === undefined ? evn.x : evn.px, - evn.py === undefined ? evn.y : evn.py - ); - maskPaintCtx.lineTo(evn.x, evn.y); - maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; - maskPaintCtx.stroke(); - } + maskPaintCtx.lineWidth = state.brushSize; + maskPaintCtx.beginPath(); + maskPaintCtx.moveTo( + evn.px === undefined ? evn.x : evn.px, + evn.py === undefined ? evn.y : evn.py + ); + maskPaintCtx.lineTo(evn.x, evn.y); + maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; + maskPaintCtx.stroke(); }; const _mask_brush_erase_callback = (evn, state) => { - if ( - (evn.initialTarget && evn.initialTarget.id === "overlayCanvas") || - (!evn.initialTarget && evn.target.id === "overlayCanvas") - ) { - maskPaintCtx.globalCompositeOperation = "destination-out"; - maskPaintCtx.strokeStyle = "black"; + maskPaintCtx.globalCompositeOperation = "destination-out"; + maskPaintCtx.strokeStyle = "black"; - maskPaintCtx.lineWidth = state.brushSize; - maskPaintCtx.beginPath(); - maskPaintCtx.moveTo( - evn.px === undefined ? evn.x : evn.px, - evn.py === undefined ? evn.y : evn.py - ); - maskPaintCtx.lineTo(evn.x, evn.y); - maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; - maskPaintCtx.stroke(); - } + maskPaintCtx.lineWidth = state.brushSize; + maskPaintCtx.beginPath(); + maskPaintCtx.moveTo( + evn.px === undefined ? evn.x : evn.px, + evn.py === undefined ? evn.y : evn.py + ); + maskPaintCtx.lineTo(evn.x, evn.y); + maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; + maskPaintCtx.stroke(); }; const maskBrushTool = () => @@ -69,27 +59,27 @@ const maskBrushTool = () => (state, opt) => { // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}}); + state.movecb({...mouse.coords.world.pos}); // Start Listeners - mouse.listen.canvas.onmousemove.on(state.movecb); - mouse.listen.canvas.onwheel.on(state.wheelcb); - mouse.listen.canvas.btn.left.onpaintstart.on(state.drawcb); - mouse.listen.canvas.btn.left.onpaint.on(state.drawcb); - mouse.listen.canvas.btn.right.onpaintstart.on(state.erasecb); - mouse.listen.canvas.btn.right.onpaint.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.movecb); + mouse.listen.world.onwheel.on(state.wheelcb); + mouse.listen.world.btn.left.onpaintstart.on(state.drawcb); + mouse.listen.world.btn.left.onpaint.on(state.drawcb); + mouse.listen.world.btn.right.onpaintstart.on(state.erasecb); + mouse.listen.world.btn.right.onpaint.on(state.erasecb); // Display Mask setMask("neutral"); }, (state, opt) => { // Clear Listeners - mouse.listen.canvas.onmousemove.clear(state.movecb); - mouse.listen.canvas.onwheel.clear(state.wheelcb); - mouse.listen.canvas.btn.left.onpaintstart.clear(state.drawcb); - mouse.listen.canvas.btn.left.onpaint.clear(state.drawcb); - mouse.listen.canvas.btn.right.onpaintstart.clear(state.erasecb); - mouse.listen.canvas.btn.right.onpaint.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.movecb); + mouse.listen.world.onwheel.clear(state.wheelcb); + mouse.listen.world.btn.left.onpaintstart.clear(state.drawcb); + mouse.listen.world.btn.left.onpaint.clear(state.drawcb); + mouse.listen.world.btn.right.onpaintstart.clear(state.erasecb); + mouse.listen.world.btn.right.onpaint.clear(state.erasecb); // Hide Mask setMask("none"); @@ -115,25 +105,24 @@ const maskBrushTool = () => state.preview = false; state.movecb = (evn) => { - if (evn.target.id === "overlayCanvas") { - // draw big translucent white blob cursor - 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"; + // 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 = "#FFFFFF50"; - ovCtx.fill(); + ovCtx.fill(); - if (state.preview) { - ovCtx.strokeStyle = "#000F"; - ovCtx.setLineDash([4, 2]); - ovCtx.stroke(); - ovCtx.setLineDash([]); - } + if (state.preview) { + ovCtx.strokeStyle = "#000F"; + ovCtx.setLineDash([4, 2]); + ovCtx.stroke(); + ovCtx.setLineDash([]); } }; state.wheelcb = (evn) => { - if (evn.target.id === "overlayCanvas") { + if (!evn.evn.ctrlKey) { state.brushSize = state.setBrushSize( state.brushSize - Math.floor(state.config.brushScrollSpeed * evn.delta) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 3b19d40..a9f4b8f 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -5,16 +5,16 @@ const selectTransformTool = () => (state, opt) => { // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}}); + state.movecb(mouse.coords.world.pos); // Canvas left mouse handlers - mouse.listen.canvas.onmousemove.on(state.movecb); - mouse.listen.canvas.btn.left.onclick.on(state.clickcb); - mouse.listen.canvas.btn.left.ondragstart.on(state.dragstartcb); - mouse.listen.canvas.btn.left.ondragend.on(state.dragendcb); + mouse.listen.world.onmousemove.on(state.movecb); + mouse.listen.world.btn.left.onclick.on(state.clickcb); + mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); + mouse.listen.world.btn.left.ondragend.on(state.dragendcb); // Canvas right mouse handler - mouse.listen.canvas.btn.right.onclick.on(state.cancelcb); + mouse.listen.world.btn.right.onclick.on(state.cancelcb); // Keyboard click handlers keyboard.listen.onkeyclick.on(state.keyclickcb); @@ -29,12 +29,12 @@ const selectTransformTool = () => }, (state, opt) => { // Clear all those listeners and shortcuts we set up - mouse.listen.canvas.onmousemove.clear(state.movecb); - mouse.listen.canvas.btn.left.onclick.clear(state.clickcb); - mouse.listen.canvas.btn.left.ondragstart.clear(state.dragstartcb); - mouse.listen.canvas.btn.left.ondragend.clear(state.dragendcb); + mouse.listen.world.onmousemove.clear(state.movecb); + mouse.listen.world.btn.left.onclick.clear(state.clickcb); + mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); + mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); - mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb); + mouse.listen.world.btn.right.onclick.clear(state.cancelcb); keyboard.listen.onkeyclick.clear(state.keyclickcb); keyboard.listen.onkeydown.clear(state.keydowncb); @@ -46,7 +46,7 @@ const selectTransformTool = () => state.reset(); // Resets cursor - ovCanvas.style.cursor = "auto"; + imageCollection.inputElement.style.cursor = "auto"; }, { init: (state) => { @@ -185,274 +185,259 @@ const selectTransformTool = () => // Mouse move handelr. As always, also renders cursor state.movecb = (evn) => { - ovCanvas.style.cursor = "auto"; + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + imageCollection.inputElement.style.cursor = "auto"; state.lastMouseTarget = evn.target; state.lastMouseMove = evn; - if (evn.target.id === "overlayCanvas") { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); - } + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, true, 64); + y += snap(evn.y, true, 64); + } - // Update scale - if (state.scaling) { - state.scaling.scaleTo(x, y, state.keepAspectRatio); - } + // Update scale + if (state.scaling) { + state.scaling.scaleTo(x, y, state.keepAspectRatio); + } - // Update position - if (state.moving) { - state.selected.x = x - state.moving.offset.x; - state.selected.y = y - state.moving.offset.y; - state.selected.updateOriginal(); - } + // Update position + if (state.moving) { + state.selected.x = x - state.moving.offset.x; + state.selected.y = y - state.moving.offset.y; + state.selected.updateOriginal(); + } - // Draw dragging box - if (state.dragging) { - ovCtx.setLineDash([2, 2]); - ovCtx.lineWidth = 1; - ovCtx.strokeStyle = "#FFF"; - - const ix = state.dragging.ix; - const iy = state.dragging.iy; - - const bb = selectionBB(ix, iy, x, y); - - ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); - ovCtx.setLineDash([]); - } - - if (state.selected) { - ovCtx.lineWidth = 1; - ovCtx.strokeStyle = "#FFF"; - - const bb = { - x: state.selected.x, - y: state.selected.y, - w: state.selected.w, - h: state.selected.h, - }; - - // Draw Image - ovCtx.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 - ); - - // Draw selection box - ovCtx.setLineDash([4, 2]); - ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); - ovCtx.setLineDash([]); - - // Draw Scaling/Rotation Origin - ovCtx.beginPath(); - ovCtx.arc( - state.selected.x + state.selected.w / 2, - state.selected.y + state.selected.h / 2, - 5, - 0, - 2 * Math.PI - ); - ovCtx.stroke(); - - // Draw Scaling Handles - let cursorInHandle = false; - state.selected.handles().forEach((handle) => { - if (handle.contains(evn.x, evn.y)) { - cursorInHandle = true; - ovCtx.strokeRect( - handle.x - 1, - handle.y - 1, - handle.w + 2, - handle.h + 2 - ); - } else { - ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h); - } - }); - - // Change cursor - if (cursorInHandle || state.selected.contains(evn.x, evn.y)) - ovCanvas.style.cursor = "pointer"; - } - - // Draw current cursor location - ovCtx.lineWidth = 3; + // Draw dragging box + if (state.dragging) { + ovCtx.setLineDash([2, 2]); + ovCtx.lineWidth = 1; ovCtx.strokeStyle = "#FFF"; - ovCtx.beginPath(); - ovCtx.moveTo(x, y + 10); - ovCtx.lineTo(x, y - 10); - ovCtx.moveTo(x + 10, y); - ovCtx.lineTo(x - 10, y); - ovCtx.stroke(); + const ix = state.dragging.ix; + const iy = state.dragging.iy; + + const bb = selectionBB(ix, iy, x, y); + + ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); + ovCtx.setLineDash([]); } + + if (state.selected) { + ovCtx.lineWidth = 1; + ovCtx.strokeStyle = "#FFF"; + + const bb = { + x: state.selected.x, + y: state.selected.y, + w: state.selected.w, + h: state.selected.h, + }; + + // Draw Image + ovCtx.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 + ); + + // Draw selection box + ovCtx.setLineDash([4, 2]); + ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); + ovCtx.setLineDash([]); + + // Draw Scaling/Rotation Origin + ovCtx.beginPath(); + ovCtx.arc( + state.selected.x + state.selected.w / 2, + state.selected.y + state.selected.h / 2, + 5, + 0, + 2 * Math.PI + ); + ovCtx.stroke(); + + // Draw Scaling Handles + let cursorInHandle = false; + state.selected.handles().forEach((handle) => { + if (handle.contains(evn.x, evn.y)) { + cursorInHandle = true; + ovCtx.strokeRect( + handle.x - 1, + handle.y - 1, + handle.w + 2, + handle.h + 2 + ); + } else { + ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h); + } + }); + + // Change cursor + if (cursorInHandle || state.selected.contains(evn.x, evn.y)) + imageCollection.inputElement.style.cursor = "pointer"; + } + + // Draw current cursor location + ovCtx.lineWidth = 3; + ovCtx.strokeStyle = "#FFF"; + + ovCtx.beginPath(); + ovCtx.moveTo(x, y + 10); + ovCtx.lineTo(x, y - 10); + ovCtx.moveTo(x + 10, y); + ovCtx.lineTo(x - 10, y); + ovCtx.stroke(); }; // Handles left mouse clicks state.clickcb = (evn) => { - if (evn.target.id === "overlayCanvas") { - // If something is selected, commit changes to the canvas - if (state.selected) { - imgCtx.drawImage( - state.selected.image, - state.original.x, - state.original.y - ); - commands.runCommand( - "eraseImage", - "Image Transform Erase", - state.original - ); - commands.runCommand( - "drawImage", - "Image Transform Draw", - state.selected - ); - state.original = null; - state.selected = null; + // If something is selected, commit changes to the canvas + if (state.selected) { + imgCtx.drawImage( + state.selected.image, + state.original.x, + state.original.y + ); + commands.runCommand( + "eraseImage", + "Image Transform Erase", + state.original + ); + commands.runCommand( + "drawImage", + "Image Transform Draw", + state.selected + ); + state.original = null; + state.selected = null; - redraw(); - } + redraw(); } }; // Handles left mouse drag events state.dragstartcb = (evn) => { - if (evn.target.id === "overlayCanvas") { - let ix = evn.ix; - let iy = evn.iy; - if (state.snapToGrid) { - ix += snap(evn.ix, true, 64); - iy += snap(evn.iy, true, 64); - } - - // If is selected, check if drag is in handles/body and act accordingly - if (state.selected) { - const handles = state.selected.handles(); - - const activeHandle = handles.find((v) => - v.contains(evn.ix, evn.iy) - ); - if (activeHandle) { - state.scaling = activeHandle; - return; - } else if (state.selected.contains(ix, iy)) { - state.moving = { - offset: {x: ix - state.selected.x, y: iy - state.selected.y}, - }; - return; - } - } - // If it is not, just create new selection - state.reset(); - state.dragging = {ix, iy}; + let ix = evn.ix; + let iy = evn.iy; + if (state.snapToGrid) { + ix += snap(evn.ix, true, 64); + iy += snap(evn.iy, true, 64); } + + // If is selected, check if drag is in handles/body and act accordingly + if (state.selected) { + const handles = state.selected.handles(); + + const activeHandle = handles.find((v) => + v.contains(evn.ix, evn.iy) + ); + if (activeHandle) { + state.scaling = activeHandle; + return; + } else if (state.selected.contains(ix, iy)) { + state.moving = { + offset: {x: ix - state.selected.x, y: iy - state.selected.y}, + }; + return; + } + } + // If it is not, just create new selection + state.reset(); + state.dragging = {ix, iy}; }; // Handles left mouse drag end events state.dragendcb = (evn) => { - if (evn.target.id === "overlayCanvas") { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); - } - - // If we are scaling, stop scaling and do some handler magic - if (state.scaling) { - state.selected.updateOriginal(); - state.scaling = null; - // If we are moving the selection, just... stop - } else if (state.moving) { - state.moving = null; - /** - * If we are dragging, create a cutout selection area and save to an auxiliar image - * We will be rendering the image to the overlay, so it will not be noticeable - */ - } else if (state.dragging) { - state.original = selectionBB( - state.dragging.ix, - state.dragging.iy, - x, - y - ); - state.selected = selectionBB( - state.dragging.ix, - state.dragging.iy, - x, - y - ); - - // Cut out selected portion of the image for manipulation - const cvs = document.createElement("canvas"); - cvs.width = state.selected.w; - cvs.height = state.selected.h; - const ctx = cvs.getContext("2d"); - - ctx.drawImage( - imgCanvas, - state.selected.x, - state.selected.y, - state.selected.w, - state.selected.h, - 0, - 0, - state.selected.w, - state.selected.h - ); - - imgCtx.clearRect( - state.selected.x, - state.selected.y, - state.selected.w, - state.selected.h - ); - state.selected.image = cvs; - state.original.image = cvs; - - if (state.selected.w === 0 || state.selected.h === 0) - state.selected = null; - - state.dragging = null; - } - redraw(); + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, true, 64); + y += snap(evn.y, true, 64); } + + // If we are scaling, stop scaling and do some handler magic + if (state.scaling) { + state.selected.updateOriginal(); + state.scaling = null; + // If we are moving the selection, just... stop + } else if (state.moving) { + state.moving = null; + /** + * If we are dragging, create a cutout selection area and save to an auxiliar image + * We will be rendering the image to the overlay, so it will not be noticeable + */ + } else if (state.dragging) { + state.original = selectionBB( + state.dragging.ix, + state.dragging.iy, + x, + y + ); + state.selected = selectionBB( + state.dragging.ix, + state.dragging.iy, + x, + y + ); + + // Cut out selected portion of the image for manipulation + const cvs = document.createElement("canvas"); + cvs.width = state.selected.w; + cvs.height = state.selected.h; + const ctx = cvs.getContext("2d"); + + ctx.drawImage( + imgCanvas, + state.selected.x, + state.selected.y, + state.selected.w, + state.selected.h, + 0, + 0, + state.selected.w, + state.selected.h + ); + + imgCtx.clearRect( + state.selected.x, + state.selected.y, + state.selected.w, + state.selected.h + ); + state.selected.image = cvs; + state.original.image = cvs; + + if (state.selected.w === 0 || state.selected.h === 0) + state.selected = null; + + state.dragging = null; + } + redraw(); }; // Handler for right clicks. Basically resets everything state.cancelcb = (evn) => { - if (evn.target.id === "overlayCanvas") { - state.reset(); - } + state.reset(); }; // Keyboard callbacks (For now, they just handle the "delete" key) state.keydowncb = (evn) => {}; state.keyclickcb = (evn) => { - if (state.lastMouseTarget.id === "overlayCanvas") { - switch (evn.code) { - case "Delete": - // Deletes selected area - state.selected && - commands.runCommand( - "eraseImage", - "Erase Area", - state.selected - ); - state.selected = null; - redraw(); - } + switch (evn.code) { + case "Delete": + // Deletes selected area + state.selected && + commands.runCommand("eraseImage", "Erase Area", state.selected); + state.selected = null; + redraw(); } }; @@ -460,47 +445,45 @@ const selectTransformTool = () => // Handles copying state.ctrlccb = (evn, cut = false) => { - if (state.selected && state.lastMouseTarget.id === "overlayCanvas") { - // We create a new canvas to store the data - state.clipboard.copy = document.createElement("canvas"); + // We create a new canvas to store the data + state.clipboard.copy = document.createElement("canvas"); - state.clipboard.copy.width = state.selected.w; - state.clipboard.copy.height = state.selected.h; + state.clipboard.copy.width = state.selected.w; + state.clipboard.copy.height = state.selected.h; - const ctx = state.clipboard.copy.getContext("2d"); + const ctx = state.clipboard.copy.getContext("2d"); - ctx.clearRect(0, 0, state.selected.w, state.selected.h); - ctx.drawImage( - state.selected.image, - 0, - 0, - state.selected.image.width, - state.selected.image.height, - 0, - 0, - state.selected.w, - state.selected.h - ); + ctx.clearRect(0, 0, state.selected.w, state.selected.h); + ctx.drawImage( + state.selected.image, + 0, + 0, + state.selected.image.width, + state.selected.image.height, + 0, + 0, + state.selected.w, + state.selected.h + ); - // If cutting, we reverse the selection and erase the selection area - if (cut) { - const aux = state.original; - state.reset(); + // If cutting, we reverse the selection and erase the selection area + if (cut) { + const aux = state.original; + state.reset(); - commands.runCommand("eraseImage", "Cut Image", aux); - } + commands.runCommand("eraseImage", "Cut Image", aux); + } - // Because firefox needs manual activation of the feature - if (state.useClipboard) { - // 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); - }); + // Because firefox needs manual activation of the feature + if (state.useClipboard) { + // 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); }); - } + }); } }; diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 0bb111a..8f87276 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -5,12 +5,12 @@ const stampTool = () => (state, opt) => { // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}}); + state.movecb({...mouse.coords.world.pos}); // Start Listeners - mouse.listen.canvas.onmousemove.on(state.movecb); - mouse.listen.canvas.btn.left.onclick.on(state.drawcb); - mouse.listen.canvas.btn.right.onclick.on(state.cancelcb); + mouse.listen.world.onmousemove.on(state.movecb); + mouse.listen.world.btn.left.onclick.on(state.drawcb); + mouse.listen.world.btn.right.onclick.on(state.cancelcb); // For calls from other tools to paste image if (opt && opt.image) { @@ -32,9 +32,9 @@ const stampTool = () => }, (state, opt) => { // Clear Listeners - mouse.listen.canvas.onmousemove.clear(state.movecb); - mouse.listen.canvas.btn.left.onclick.clear(state.drawcb); - mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb); + mouse.listen.world.onmousemove.clear(state.movecb); + mouse.listen.world.btn.left.onclick.clear(state.drawcb); + mouse.listen.world.btn.right.onclick.clear(state.cancelcb); // Deselect state.selected = null; @@ -153,73 +153,69 @@ const stampTool = () => }; state.movecb = (evn) => { - if (evn.target && evn.target.id === "overlayCanvas") { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); - } - - state.lastMouseMove = evn; - - // Draw selected image - if (state.selected) { - ovCtx.drawImage(state.selected.image, x, y); - } - - // Draw current cursor location - ovCtx.lineWidth = 3; - ovCtx.strokeStyle = "#FFF"; - - ovCtx.beginPath(); - ovCtx.moveTo(x, y + 10); - ovCtx.lineTo(x, y - 10); - ovCtx.moveTo(x + 10, y); - ovCtx.lineTo(x - 10, y); - ovCtx.stroke(); + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, true, 64); + y += snap(evn.y, true, 64); } + + state.lastMouseMove = evn; + + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + + // Draw selected image + if (state.selected) { + ovCtx.drawImage(state.selected.image, x, y); + } + + // Draw current cursor location + ovCtx.lineWidth = 3; + ovCtx.strokeStyle = "#FFF"; + + ovCtx.beginPath(); + ovCtx.moveTo(x, y + 10); + ovCtx.lineTo(x, y - 10); + ovCtx.moveTo(x + 10, y); + ovCtx.lineTo(x - 10, y); + ovCtx.stroke(); }; state.drawcb = (evn) => { - if (evn.target.id === "overlayCanvas") { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); - } + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, true, 64); + y += snap(evn.y, true, 64); + } - const resource = state.selected; + const resource = state.selected; - if (resource) { - commands.runCommand("drawImage", "Image Stamp", { - image: resource.image, - x, - y, - }); + if (resource) { + commands.runCommand("drawImage", "Image Stamp", { + image: resource.image, + x, + y, + }); - if (resource.temporary) state.deleteResource(resource.id); - } + if (resource.temporary) state.deleteResource(resource.id); + } - if (state.back) { - toolbar.unlock(); - const backfn = state.back; - state.back = null; - backfn({message: "Returning from stamp", pasted: true}); - } + if (state.back) { + toolbar.unlock(); + const backfn = state.back; + state.back = null; + backfn({message: "Returning from stamp", pasted: true}); } }; state.cancelcb = (evn) => { - if (evn.target.id === "overlayCanvas") { - state.selectResource(null); + state.selectResource(null); - if (state.back) { - toolbar.unlock(); - const backfn = state.back; - state.back = null; - backfn({message: "Returning from stamp", pasted: false}); - } + if (state.back) { + toolbar.unlock(); + const backfn = state.back; + state.back = null; + backfn({message: "Returning from stamp", pasted: false}); } }; diff --git a/js/util.d.ts b/js/util.d.ts deleted file mode 100644 index e4cb07f..0000000 --- a/js/util.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Generates a random string in the following format: - * - * xxxx-xxxx-xxxx-...-xxxx - * - * @param size number of character quartets to generate - * @return Generated ID - */ -declare function guid(size: number): string; - -/** - * Sets default values for options parameters - * - * @param options An object received as a parameter - * @param defaults An object with default values for each expected key - * @return The original options parameter - */ -declare function defaultOpt( - options: {[key: string]: any}, - defaults: {[key: string]: any} -): {[key: string]: any}; - -/** - * Sets default values for options parameters - * - * @param options An object received as a parameter - * @param defaults An object with default values for each expected key - * @return The original options parameter - */ -declare function makeReadOnly( - options: {[key: string]: any}, - defaults: {[key: string]: any} -): {[key: string]: any}; - -/** - * Makes an object read-only, throwing an exception when attempting to set - * - * @param obj Object to be proxied - * @param name Name of the object, for logging purposes - * @return The proxied object - */ -declare function makeReadOnly(obj: object, name?: string): object; - -/** - * Makes an object have each key be writeable only once, throwing an exception when - * attempting to set an existing parameter - * - * @param obj Object to be proxied - * @param name Name of the object, for logging purposes - * @return The proxied object - */ -declare function makeWriteOnce(obj: object, name?: string): object;