diff --git a/css/ui/layers.css b/css/ui/layers.css index 376af52..cadf9c1 100644 --- a/css/ui/layers.css +++ b/css/ui/layers.css @@ -132,3 +132,72 @@ flex: 1; height: 25px; } + +/* Resizing buttons */ +.expand-button { + display: flex; + + align-items: center; + justify-content: center; + + margin: 0; + padding: 0; + border: 0; + + background-color: transparent; + + cursor: pointer; + + transition-duration: 300ms; + + border: 2px solid #293d3d30; +} + +.expand-button::after { + content: ""; + + background-color: #293d3d77; + + mask-image: url("/res/icons/chevron-up.svg"); + mask-size: contain; + + width: 60px; + height: 60px; +} + +.expand-button:hover::after { + background-color: #466; +} + +.expand-button.right::after { + transform: rotate(90deg); +} + +.expand-button.bottom::after { + transform: rotate(180deg); +} + +.expand-button.left::after { + transform: rotate(270deg); +} + +.expand-button.left { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +.expand-button.top { + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} +.expand-button.right { + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} +.expand-button.bottom { + border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; +} + +.expand-button:hover { + background-color: #293d3d77; +} diff --git a/js/index.js b/js/index.js index 212561d..085cb31 100644 --- a/js/index.js +++ b/js/index.js @@ -343,17 +343,14 @@ function newImage(evt) { clearPaintedMask(); uil.layers.forEach(({layer}) => { commands.runCommand("eraseImage", "Clear Canvas", { - x: 0, - y: 0, - w: layer.canvas.width, - h: layer.canvas.height, + ...layer.bb, ctx: layer.ctx, }); }); } function clearPaintedMask() { - maskPaintCtx.clearRect(0, 0, maskPaintCanvas.width, maskPaintCanvas.height); + maskPaintLayer.clear(); } function march(bb, options = {}) { @@ -558,8 +555,16 @@ function drawBackground() { // Checkerboard let darkTileColor = "#333"; let lightTileColor = "#555"; - for (var x = 0; x < bgLayer.canvas.width; x += 64) { - for (var y = 0; y < bgLayer.canvas.height; y += 64) { + for ( + var x = -bgLayer.origin.x - 64; + x < bgLayer.canvas.width - bgLayer.origin.x; + x += 64 + ) { + for ( + var y = -bgLayer.origin.y - 64; + y < bgLayer.canvas.height - bgLayer.origin.y; + y += 64 + ) { bgLayer.ctx.fillStyle = (x + y) % 128 === 0 ? lightTileColor : darkTileColor; bgLayer.ctx.fillRect(x, y, 64, 64); diff --git a/js/initalize/debug.populate.js b/js/initalize/debug.populate.js index 8033776..1e27b7d 100644 --- a/js/initalize/debug.populate.js +++ b/js/initalize/debug.populate.js @@ -17,6 +17,19 @@ mouse.listen.world.onmousemove.on((evn) => { canvasYInfo.textContent = evn.y; snapXInfo.textContent = evn.x + snap(evn.x); snapYInfo.textContent = evn.y + snap(evn.y); + + if (debug) { + debugLayer.clear(); + debugCtx.fillStyle = "#F0F"; + debugCtx.beginPath(); + debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2); + debugCtx.fill(); + + debugCtx.fillStyle = "#0FF"; + debugCtx.beginPath(); + debugCtx.arc(evn.x, evn.y, 5, 0, Math.PI * 2); + debugCtx.fill(); + } }); /** diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index ae5b974..917f8ff 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -53,44 +53,97 @@ uiCanvas.width = uiCanvas.clientWidth; uiCanvas.height = uiCanvas.clientHeight; const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); -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. + * Here we setup canvas dynamic scaling */ -mouse.registerContext( - "world", - (evn, ctx) => { - // Fix because in chrome layerX and layerY simply doesnt work - ctx.coords.prev.x = ctx.coords.pos.x; - ctx.coords.prev.y = ctx.coords.pos.y; - - // Get element bounding rect - const bb = imageCollection.element.getBoundingClientRect(); - - // Get element width/height (css, cause I don't trust client sizes in chrome anymore) - const w = imageCollection.size.w; - const h = imageCollection.size.h; - - // Get cursor position - const x = evn.clientX; - const y = evn.clientY; - - // Map to layer space - const layerX = ((x - bb.left) / bb.width) * w; - const layerY = ((y - bb.top) / bb.height) * h; +(() => { + let expandSize = localStorage.getItem("expand-size") || 1024; + expandSize = parseInt(expandSize, 10); + + const askSize = () => { + const by = prompt("How much do you want to expand by?", expandSize); + + if (!by) return null; + else { + const len = parseInt(by, 10); + localStorage.setItem("expand-size", len); + expandSize = len; + return len; + } + }; + + const leftButton = makeElement("button", -64, 0); + leftButton.classList.add("expand-button", "left"); + leftButton.style.width = "64px"; + leftButton.style.height = `${imageCollection.size.h}px`; + leftButton.addEventListener("click", () => { + let size = null; + if ((size = askSize())) { + imageCollection.expand(size, 0, 0, 0); + drawBackground(); + const newLeft = -imageCollection.inputOffset.x - imageCollection.origin.x; + leftButton.style.left = newLeft - 64 + "px"; + topButton.style.left = newLeft + "px"; + bottomButton.style.left = newLeft + "px"; + topButton.style.width = imageCollection.size.w + "px"; + bottomButton.style.width = imageCollection.size.w + "px"; + } + }); + + const rightButton = makeElement("button", imageCollection.size.w, 0); + rightButton.classList.add("expand-button", "right"); + rightButton.style.width = "64px"; + rightButton.style.height = `${imageCollection.size.h}px`; + rightButton.addEventListener("click", () => { + let size = null; + if ((size = askSize())) { + imageCollection.expand(0, 0, size, 0); + drawBackground(); + rightButton.style.left = + parseInt(rightButton.style.left, 10) + size + "px"; + topButton.style.width = imageCollection.size.w + "px"; + bottomButton.style.width = imageCollection.size.w + "px"; + } + }); + + const topButton = makeElement("button", 0, -64); + topButton.classList.add("expand-button", "top"); + topButton.style.height = "64px"; + topButton.style.width = `${imageCollection.size.w}px`; + topButton.addEventListener("click", () => { + let size = null; + if ((size = askSize())) { + imageCollection.expand(0, size, 0, 0); + drawBackground(); + const newTop = -imageCollection.inputOffset.y - imageCollection.origin.y; + topButton.style.top = newTop - 64 + "px"; + leftButton.style.top = newTop + "px"; + rightButton.style.top = newTop + "px"; + leftButton.style.height = imageCollection.size.h + "px"; + rightButton.style.height = imageCollection.size.h + "px"; + } + }); + + const bottomButton = makeElement("button", 0, imageCollection.size.h); + bottomButton.classList.add("expand-button", "bottom"); + bottomButton.style.height = "64px"; + bottomButton.style.width = `${imageCollection.size.w}px`; + bottomButton.addEventListener("click", () => { + let size = null; + if ((size = askSize())) { + imageCollection.expand(0, 0, 0, size); + drawBackground(); + bottomButton.style.top = + parseInt(bottomButton.style.top, 10) + size + "px"; + leftButton.style.height = imageCollection.size.h + "px"; + rightButton.style.height = imageCollection.size.h + "px"; + } + }); +})(); - // - ctx.coords.pos.x = Math.round(layerX); - ctx.coords.pos.y = Math.round(layerY); - }, - {target: imageCollection.inputElement} -); +debugLayer.hide(); // Hidden by default +// Where CSS and javascript magic happens to make the canvas viewport work /** * The global viewport object (may be modularized in the future). All * coordinates given are of the center of the viewport @@ -158,6 +211,31 @@ let worldInit = null; viewport.transform(imageCollection.element); +/** + * 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) => { + // Fix because in chrome layerX and layerY simply doesnt work + ctx.coords.prev.x = ctx.coords.pos.x; + ctx.coords.prev.y = ctx.coords.pos.y; + + // Get cursor position + const x = evn.clientX; + const y = evn.clientY; + + // Map to layer space + const layerCoords = viewport.viewToCanvas(x, y); + + // Set coords + ctx.coords.pos.x = Math.round(layerCoords.x); + ctx.coords.pos.y = Math.round(layerCoords.y); + }, + {target: imageCollection.inputElement} +); + mouse.listen.window.onwheel.on((evn) => { if (evn.evn.ctrlKey) { evn.evn.preventDefault(); @@ -176,14 +254,6 @@ mouse.listen.window.onwheel.on((evn) => { viewport.transform(imageCollection.element); toolbar.currentTool.redraw(); - - if (debug) { - debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); - debugCtx.fillStyle = "#F0F"; - debugCtx.beginPath(); - debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2); - debugCtx.fill(); - } } }); @@ -197,8 +267,14 @@ mouse.listen.window.btn.middle.onpaint.on((evn) => { 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); + viewport.cx = Math.max( + Math.min(viewport.cx, imageCollection.size.w - imageCollection.origin.x), + -imageCollection.origin.x + ); + viewport.cy = Math.max( + Math.min(viewport.cy, imageCollection.size.h - imageCollection.origin.y), + -imageCollection.origin.y + ); // Draw Viewport location } diff --git a/js/lib/layers.d.js b/js/lib/layers.d.js new file mode 100644 index 0000000..b847fd7 --- /dev/null +++ b/js/lib/layers.d.js @@ -0,0 +1,47 @@ +/** + * A layer + * + * @typedef {object} Layer + * @property {string} id The id of the layer + * @property {string} key A identifier for the layer + * @property {string} name The display name of the layer + * @property {BoundingBox} bb The current bounding box of the layer, in layer coordinates + * @property {Size} resolution The resolution of the layer (canvas) + * @property {boolean} full If the layer is a full layer (occupies the full collection) + * @property {number} x The x coordinate of the layer + * @property {number} y The y coordinate of the layer + * @property {number} width The width of the layer + * @property {number} w The width of the layer + * @property {number} height The height of the layer + * @property {number} h The height of the layer + * @property {Point} origin The location of the origin ((0, 0) point) of the layer (If canvas goes from -64, -32 to 128, 512, it's (64, 32)) + * @property {HTMLCanvasElement} canvas The canvas element of the layers + * @property {CanvasRenderingContext2D} ctx The context of the canvas of the layer + * @property {function} clear Clears the layer contents + * @property {function} moveAfter Moves this layer to another level (after given layer) + * @property {function} moveBefore Moves this layer to another level (before given layer) + * @property {function} moveTo Moves this layer to another location + * @property {function} resize Resizes the layer in place + * @property {function} hide Hides the layer + * @property {function} unhide Unhides the layer + */ + +/** + * A layer collection + * + * @typedef {object} LayerCollection + * @property {string} id The id of the collection + * @property {string} key A identifier for the collection + * @property {string} name The display name of the collection + * @property {HTMLDivElement} element The base element of the collection + * @property {HTMLDivElement} inputElement The element used for input handling for the collection + * @property {Point} inputOffset The offset for calculating layer coordinates from input element input information + * @property {Point} origin The location of the origin ((0, 0) point) of the collection (If canvas goes from -64, -32 to 128, 512, it's (64, 32)) + * @property {BoundingBox} bb The current bounding box of the collection, in layer coordinates + * @property {{[key: string]: Layer}} layers An object for quick access to named layers of the collection + * @property {Size} size The size of the collection (CSS) + * @property {Size} resolution The resolution of the collection (canvas) + * @property {function} expand Expands the collection and its full layers by the specified amounts + * @property {function} registerLayer Registers a new layer + * @property {function} deleteLayer Deletes a layer from the collection + */ diff --git a/js/lib/layers.js b/js/lib/layers.js index eedd272..779cc19 100644 --- a/js/lib/layers.js +++ b/js/lib/layers.js @@ -3,6 +3,162 @@ * * It manages canvases and their locations and sizes according to current viewport views */ +/** + * Here is where the old magic is created. + * + * This is probably not recommended, but it works and + * is probably the most reliable way to not break everything. + */ +(() => { + const original = { + drawImage: CanvasRenderingContext2D.prototype.drawImage, + getImageData: CanvasRenderingContext2D.prototype.getImageData, + putImageData: CanvasRenderingContext2D.prototype.putImageData, + + // Drawing methods + moveTo: CanvasRenderingContext2D.prototype.moveTo, + lineTo: CanvasRenderingContext2D.prototype.lineTo, + + arc: CanvasRenderingContext2D.prototype.arc, + fillRect: CanvasRenderingContext2D.prototype.fillRect, + clearRect: CanvasRenderingContext2D.prototype.clearRect, + }; + + // Backing up original functions to Root + Object.keys(original).forEach((key) => { + CanvasRenderingContext2D.prototype[key + "Root"] = function (...args) { + return original[key].call(this, ...args); + }; + }); + + // Add basic get bounding box support (canvas coordinates) + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "bb", { + get: function () { + return new BoundingBox({ + x: -this.origin.x, + y: -this.origin.y, + w: this.canvas.width, + h: this.canvas.height, + }); + }, + }); + + // Modifying drawImage + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "drawImage", { + value: function (...args) { + switch (args.length) { + case 3: + case 5: + if (this.origin !== undefined) { + args[1] += this.origin.x; + args[2] += this.origin.y; + } + break; + case 9: + // Check for origin on source + const sctx = args[0].getContext && args[0].getContext("2d"); + if (sctx && sctx.origin !== undefined) { + args[1] += sctx.origin.x; + args[2] += sctx.origin.y; + } + + // Check for origin on destination + if (this.origin !== undefined) { + args[5] += this.origin.x; + args[6] += this.origin.y; + } + break; + } + // Pass arguments through + return original.drawImage.call(this, ...args); + }, + }); + + // Modifying getImageData method + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "getImageData", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.getImageData.call(this, ...args); + }, + }); + + // Modifying putImageData method + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "putImageData", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.putImageData.call(this, ...args); + }, + }); + + // Modifying moveTo method + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "moveTo", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.moveTo.call(this, ...args); + }, + }); + + // Modifying lineTo method + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "lineTo", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.lineTo.call(this, ...args); + }, + }); + + // Modifying arc + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "arc", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.arc.call(this, ...args); + }, + }); + + // Modifying fillRect + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "fillRect", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.fillRect.call(this, ...args); + }, + }); + // Modifying clearRect + Reflect.defineProperty(CanvasRenderingContext2D.prototype, "clearRect", { + value: function (...args) { + if (this.origin) { + args[0] += this.origin.x; + args[1] += this.origin.y; + } + // Pass arguments through + return original.clearRect.call(this, ...args); + }, + }); +})(); +// End of black magic + const layers = { _collections: [], collections: makeWriteOnce({}, "layers.collections"), @@ -17,6 +173,18 @@ const layers = { // Registers a new collection // Layer collections are a group of layers (canvases) that are rendered in tandem. (same width, height, position, transform, etc) + /** + * + * @param {string} key A key used to identify the collection + * @param {Size} size The initial size of the collection in pixels (CSS size) + * @param {object} options Extra options for the collection + * @param {string} [options.name=key] The display name of the collection + * @param {{key: string, options: object}} [options.initLayer] The configuration for the initial layer to be created + * @param {number} [options.inputSizeMultiplier=9] Size of the input area element, in pixels + * @param {HTMLElement} [options.targetElement] Element the collection will be inserted into + * @param {Size} [options.resolution=size] The resolution of the collection (canvas size). Not sure it works. + * @returns {LayerCollection} The newly created layer collection + */ registerCollection: (key, size, options = {}) => { defaultOpt(options, { // Display name for the collection @@ -29,7 +197,7 @@ const layers = { }, // Input multiplier (Size of the input element div) - inputSizeMultiplier: 3, + inputSizeMultiplier: 9, // Target targetElement: document.getElementById("layer-render"), @@ -64,6 +232,7 @@ const layers = { options.targetElement.appendChild(element); + /** @type {LayerCollection} */ const collection = makeWriteOnce( { id, @@ -73,6 +242,7 @@ const layers = { _layers: [], layers: {}, + key, name: options.name, element, inputElement: inputel, @@ -81,11 +251,30 @@ const layers = { return this._inputOffset; }, + _origin: {x: 0, y: 0}, + get origin() { + return {...this._origin}; + }, + + get bb() { + return new BoundingBox({ + x: -this.origin.x, + y: -this.origin.y, + w: this.size.w, + h: this.size.h, + }); + }, + _resizeInputDiv() { // Set offset + const oldOffset = {...this._inputOffset}; this._inputOffset = { - x: -Math.floor(options.inputSizeMultiplier / 2) * size.w, - y: -Math.floor(options.inputSizeMultiplier / 2) * size.h, + x: + -Math.floor(options.inputSizeMultiplier / 2) * size.w - + this._origin.x, + y: + -Math.floor(options.inputSizeMultiplier / 2) * size.h - + this._origin.y, }; // Resize the input element @@ -97,6 +286,48 @@ const layers = { this.inputElement.style.height = `${ size.h * options.inputSizeMultiplier }px`; + + // Move elements inside to new offset + for (const child of this.inputElement.children) { + if (child.style.position === "absolute") { + child.style.left = `${ + parseInt(child.style.left, 10) + + oldOffset.x - + this._inputOffset.x + }px`; + child.style.top = `${ + parseInt(child.style.top, 10) + + oldOffset.y - + this._inputOffset.y + }px`; + } + } + }, + + /** + * Expands the collection and its full layers by the specified amounts + * + * @param {number} left Pixels to expand left + * @param {number} top Pixels to expand top + * @param {number} right Pixels to expand right + * @param {number} bottom Pixels to expand bottom + */ + expand(left, top, right, bottom) { + this._layers.forEach((layer) => { + if (layer.full) layer._expand(left, top, right, bottom); + }); + + this._origin.x += left; + this._origin.y += top; + + this.size.w += left + right; + this.size.h += top + bottom; + + this._resizeInputDiv(); + + for (const layer of this._layers) { + layer.moveTo(layer.x, layer.y); + } }, size, @@ -113,9 +344,9 @@ const layers = { * @param {?string} options.group * @param {object} options.after * @param {object} options.ctxOptions - * @returns + * @returns {Layer} The newly created layer */ - registerLayer: (key = null, options = {}) => { + registerLayer(key = null, options = {}) { // Make ID const id = guid(); @@ -124,7 +355,12 @@ const layers = { name: key || `Temporary ${id}`, // Bounding box for layer - bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h}, + bb: { + x: -collection.origin.x, + y: -collection.origin.y, + w: collection.size.w, + h: collection.size.h, + }, // Resolution for layer resolution: null, @@ -139,11 +375,25 @@ const layers = { ctxOptions: {}, }); - // Calculate resolution + // Check if the layer is full + let full = false; + if ( + options.bb.x === -collection.origin.x && + options.bb.y === -collection.origin.y && + options.bb.w === collection.size.w && + options.bb.h === collection.size.h + ) + full = true; + if (!options.resolution) + // Calculate resolution options.resolution = { - w: (collection.resolution.w / collection.size.w) * options.bb.w, - h: (collection.resolution.h / collection.size.h) * options.bb.h, + w: Math.round( + (collection.resolution.w / collection.size.w) * options.bb.w + ), + h: Math.round( + (collection.resolution.h / collection.size.h) * options.bb.h + ), }; // This layer's canvas @@ -166,7 +416,21 @@ const layers = { options.after.canvas.after(canvas); } + /** + * Here we set the context origin for using the black magic. + */ const ctx = canvas.getContext("2d", options.ctxOptions); + if (full) { + // Modify context to add origin information + ctx.origin = { + get x() { + return collection.origin.x; + }, + get y() { + return collection.origin.y; + }, + }; + } // Path used for logging purposes const _layerlogpath = key @@ -177,9 +441,16 @@ const layers = { _logpath: _layerlogpath, _collection: collection, + _bb: new BoundingBox(options.bb), + get bb() { + return new BoundingBox(this._bb); + }, + + resolution: new Size(options.resolution), id, key, name: options.name, + full, state: new Proxy( {visible: true}, @@ -195,10 +466,81 @@ const layers = { } ), + get x() { + return this._bb.x; + }, + + get y() { + return this._bb.y; + }, + + get width() { + return this._bb.w; + }, + + get height() { + return this._bb.h; + }, + + get w() { + return this._bb.w; + }, + + get h() { + return this._bb.h; + }, + + get origin() { + return this._collection.origin; + }, + /** Our canvas */ canvas, ctx, + /** + * This is called by the collection when the layer must be expanded. + * + * Should NOT be called directly + * + * @param {number} left Pixels to expand left + * @param {number} top Pixels to expand top + * @param {number} right Pixels to expand right + * @param {number} bottom Pixels to expand bottom + */ + _expand(left, top, right, bottom) { + const tmpCanvas = document.createElement("canvas"); + tmpCanvas.width = this.w; + tmpCanvas.height = this.h; + tmpCanvas.getContext("2d").drawImage(this.canvas, 0, 0); + + this.resize(this.w + left + right, this.h + top + bottom); + this.clear(); + this.ctx.drawImageRoot(tmpCanvas, left, top); + + this.moveTo(this.x - left, this.y - top); + }, + + /** + * Clears the layer contents + */ + clear() { + this.ctx.clearRectRoot( + 0, + 0, + this.canvas.width, + this.canvas.height + ); + }, + + /** + * Recalculates DOM positioning + */ + syncDOM() { + this.moveTo(this.x, this.y); + this.resize(this.w, this.h); + }, + /** * Moves this layer to another level (after given layer) * @@ -224,8 +566,10 @@ const layers = { * @param {number} y Y coordinate of the top left of the canvas */ moveTo(x, y) { - canvas.style.left = `${x}px`; - canvas.style.top = `${y}px`; + this._bb.x = x; + this._bb.y = y; + this.canvas.style.left = `${x}px`; + this.canvas.style.top = `${y}px`; }, /** @@ -241,6 +585,8 @@ const layers = { canvas.height = Math.round( options.resolution.h * (h / options.bb.h) ); + this._bb.w = w; + this._bb.h = h; canvas.style.width = `${w}px`; canvas.style.height = `${h}px`; }, @@ -282,7 +628,11 @@ const layers = { return layer; }, - // Deletes a layer + /** + * Deletes a layer from the collection + * + * @param {Layer} layer Layer to delete + */ deleteLayer: (layer) => { const lobj = collection._layers.splice( collection._layers.findIndex( diff --git a/js/lib/util.js b/js/lib/util.js index 83a014e..f6d5c60 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -10,6 +10,19 @@ * @property {number} y - y coordinate */ +/** + * Represents a size + */ +class Size { + w = 0; + h = 0; + + constructor({w, h} = {w: 0, h: 0}) { + this.w = w; + this.h = h; + } +} + /** * Represents a simple bouding box */ @@ -231,7 +244,12 @@ function makeWriteOnce(obj, name = "write-once object", exceptions = []) { * @returns an offset, in which [i + offset = (a location snapped to the grid)] */ function snap(i, offset = 0, gridSize = 64) { - const modulus = (i - offset) % gridSize; + let diff = i - offset; + if (diff < 0) { + diff += gridSize * Math.ceil(Math.abs(diff / gridSize)); + } + + const modulus = diff % gridSize; var snapOffset = modulus; if (modulus > gridSize / 2) snapOffset = modulus - gridSize; @@ -288,14 +306,19 @@ function cropCanvas(sourceCanvas, options = {}) { const w = sourceCanvas.width; const h = sourceCanvas.height; - var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h); + const srcCtx = sourceCanvas.getContext("2d"); + const offset = { + x: (srcCtx.origin && -srcCtx.origin.x) || 0, + y: (srcCtx.origin && -srcCtx.origin.y) || 0, + }; + var imageData = srcCtx.getImageDataRoot(0, 0, w, h); /** @type {BoundingBox} */ const bb = new BoundingBox(); - let minx = w; - let maxx = -1; - let miny = h; - let maxy = -1; + let minx = Infinity; + let maxx = -Infinity; + let miny = Infinity; + let maxy = -Infinity; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { @@ -303,10 +326,10 @@ function cropCanvas(sourceCanvas, options = {}) { const index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords //this part i get, this is checking that 4th RGBA byte for opacity if (imageData.data[index + 3] > 0) { - minx = Math.min(minx, x); - maxx = Math.max(maxx, x); - miny = Math.min(miny, y); - maxy = Math.max(maxy, y); + minx = Math.min(minx, x + offset.x); + maxx = Math.max(maxx, x + offset.x); + miny = Math.min(miny, y + offset.y); + maxy = Math.max(maxy, y + offset.y); } } } @@ -316,7 +339,8 @@ function cropCanvas(sourceCanvas, options = {}) { bb.w = maxx - minx + 1 + 2 * options.border; bb.h = maxy - miny + 1 + 2 * options.border; - if (maxx < 0) throw new NoContentError("Canvas has no content to crop"); + if (!Number.isFinite(maxx)) + throw new NoContentError("Canvas has no content to crop"); var cutCanvas = document.createElement("canvas"); cutCanvas.width = bb.w; @@ -339,12 +363,7 @@ function cropCanvas(sourceCanvas, options = {}) { function downloadCanvas(options = {}) { defaultOpt(options, { cropToContent: true, - canvas: uil.getVisible({ - x: 0, - y: 0, - w: imageCollection.size.w, - h: imageCollection.size.h, - }), + canvas: uil.getVisible(imageCollection.bb), filename: new Date() .toISOString() @@ -360,6 +379,7 @@ function downloadCanvas(options = {}) { var croppedCanvas = options.cropToContent ? cropCanvas(options.canvas).canvas : options.canvas; + if (croppedCanvas != null) { croppedCanvas.toBlob((blob) => { link.href = URL.createObjectURL(blob); diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index b3b7805..da27659 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -41,7 +41,7 @@ const colorBrushTool = () => "Color Brush", (state, opt) => { // Draw new cursor immediately - uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.movecb({ ...mouse.coords.world.pos, evn: { @@ -64,11 +64,11 @@ const colorBrushTool = () => after: imgLayer, ctxOptions: {willReadFrequently: true}, }); + state.drawLayer.canvas.style.filter = "opacity(70%)"; state.eraseLayer = imageCollection.registerLayer(null, { after: imgLayer, ctxOptions: {willReadFrequently: true}, }); - state.eraseLayer.canvas.style.display = "none"; state.eraseLayer.hide(); state.eraseBackup = imageCollection.registerLayer(null, { after: imgLayer, @@ -209,7 +209,7 @@ const colorBrushTool = () => state.brushSize - Math.floor(state.config.brushScrollSpeed * evn.delta) ); - uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.movecb(evn); } }; @@ -231,12 +231,8 @@ const colorBrushTool = () => state.keyupcb = (evn) => { switch (evn.code) { case "ShiftLeft": - if (!keyboard.isPressed("ShiftRight")) { - state.disableDropper(); - } - break; case "ShiftRight": - if (!keyboard.isPressed("ShiftLeft")) { + if (!keyboard.isPressed(evn.code)) { state.disableDropper(); } break; @@ -288,6 +284,7 @@ const colorBrushTool = () => const cropped = cropCanvas(canvas, {border: 10}); const bb = cropped.bb; + commands.runCommand("drawImage", "Color Brush Draw", { image: cropped.canvas, ...bb, @@ -302,10 +299,9 @@ const colorBrushTool = () => if (state.affectMask) _mask_brush_erase_callback(evn, state); // Make a backup of the current image to apply erase later - const bkpcanvas = state.eraseBackup.canvas; const bkpctx = state.eraseBackup.ctx; - bkpctx.clearRect(0, 0, bkpcanvas.width, bkpcanvas.height); - bkpctx.drawImage(uil.canvas, 0, 0); + state.eraseBackup.clear(); + bkpctx.drawImageRoot(uil.canvas, 0, 0); uil.ctx.globalCompositeOperation = "destination-out"; _color_brush_erase_callback(evn, state, uil.ctx); @@ -335,8 +331,8 @@ const colorBrushTool = () => const bb = cropped.bb; uil.ctx.filter = null; - uil.ctx.clearRect(0, 0, uil.canvas.width, uil.canvas.height); - uil.ctx.drawImage(bkpcanvas, 0, 0); + uil.layer.clear(); + uil.ctx.drawImageRoot(bkpcanvas, 0, 0); commands.runCommand("eraseImage", "Color Brush Erase", { mask: cropped.canvas, diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 0a5bdc0..106128e 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -39,12 +39,13 @@ const _monitorProgress = (bb, oncheck = null) => { oncheck && oncheck(data); + layer.clear(); + // 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); @@ -295,8 +296,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { }); const redraw = (url = images[at]) => { - if (url === null) - layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + if (url === null) layer.clear(); if (!url) return; const img = new Image(); @@ -318,7 +318,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { ctx.drawImage(keepUnmaskCanvas, 0, 0); } - layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + layer.clear(); layer.ctx.drawImage( canvas, 0, @@ -770,8 +770,8 @@ const dream_generate_callback = async (bb, resolution, state) => { bbCtx.globalCompositeOperation = "destination-in"; bbCtx.drawImage( maskPaintCanvas, - bb.x, - bb.y, + bb.x + maskPaintLayer.origin.x, + bb.y + maskPaintLayer.origin.y, bb.w, bb.h, 0, @@ -798,8 +798,8 @@ const dream_generate_callback = async (bb, resolution, state) => { bbCtx.globalCompositeOperation = "destination-out"; // ??? bbCtx.drawImage( maskPaintCanvas, - bb.x, - bb.y, + bb.x + maskPaintLayer.origin.x, + bb.y + maskPaintLayer.origin.y, bb.w, bb.h, 0, @@ -920,7 +920,17 @@ const dream_img2img_callback = (bb, resolution, state) => { bbCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; bbCtx.fillRect(0, 0, bb.w, bb.h); bbCtx.globalCompositeOperation = "destination-out"; - bbCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + bbCtx.drawImage( + maskPaintCanvas, + bb.x + maskPaintLayer.origin.x, + bb.y + maskPaintLayer.origin.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); bbCtx.globalCompositeOperation = "destination-atop"; bbCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; diff --git a/js/ui/tool/interrogate.js b/js/ui/tool/interrogate.js index 209ec71..2818196 100644 --- a/js/ui/tool/interrogate.js +++ b/js/ui/tool/interrogate.js @@ -4,10 +4,8 @@ const interrogateTool = () => "Interrogate", (state, opt) => { // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.mousemovecb({ - ...mouse.coords.world.pos, - }); + ovLayer.clear(); + state.redraw(); // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); @@ -37,8 +35,7 @@ const interrogateTool = () => state.invertMask = false; state.overMaskPx = 0; - state.erasePrevReticle = () => - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + state.erasePrevReticle = () => ovLayer.clear(); state.mousemovecb = (evn) => { state.erasePrevReticle(); diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index bf71cfa..46cb7cd 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -240,12 +240,7 @@ const maskBrushTool = () => clearMaskButton.textContent = "Clear"; clearMaskButton.title = "Clears Painted Mask"; clearMaskButton.onclick = () => { - maskPaintCtx.clearRect( - 0, - 0, - maskPaintCanvas.width, - maskPaintCanvas.height - ); + maskPaintLayer.clear(); }; const previewMaskButton = document.createElement("button"); diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 7def02e..dd228f3 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -4,7 +4,7 @@ const selectTransformTool = () => "Select Image", (state, opt) => { // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); state.movecb(mouse.coords.world.pos); // Canvas left mouse handlers @@ -46,7 +46,7 @@ const selectTransformTool = () => state.reset(); // Resets cursor - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); // Clears overlay imageCollection.inputElement.style.cursor = "auto"; @@ -80,7 +80,7 @@ const selectTransformTool = () => state.lastMouseMove = null; state.redraw = () => { - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); state.movecb(state.lastMouseMove); }; @@ -186,7 +186,7 @@ const selectTransformTool = () => // Mouse move handler. As always, also renders cursor state.movecb = (evn) => { - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); imageCollection.inputElement.style.cursor = "auto"; state.lastMouseTarget = evn.target; diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 6aac513..47af52d 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -6,7 +6,7 @@ const stampTool = () => state.loaded = true; // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); state.movecb({...mouse.coords.world.pos}); // Start Listeners @@ -47,7 +47,7 @@ const stampTool = () => child.classList.remove("active"); }); - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); }, { init: (state) => { @@ -88,7 +88,7 @@ const stampTool = () => state.selected = null; } - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); if (state.loaded) state.movecb(state.lastMouseMove); }; @@ -300,7 +300,7 @@ const stampTool = () => state.lastMouseMove = evn; - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + ovLayer.clear(); // Draw selected image if (state.selected) {