Merge pull request #108 from zero01101/testing

dynamic canvas scaling?
This commit is contained in:
tim h 2022-12-18 17:18:24 -06:00 committed by GitHub
commit dd52529b6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 701 additions and 123 deletions

View file

@ -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;
}

View file

@ -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);

View file

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

View file

@ -53,44 +53,97 @@ uiCanvas.width = uiCanvas.clientWidth;
uiCanvas.height = uiCanvas.clientHeight;
const uiCtx = uiCanvas.getContext("2d", {desynchronized: true});
/**
* Here we setup canvas dynamic scaling
*/
(() => {
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";
}
});
})();
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) => {
// 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;
//
ctx.coords.pos.x = Math.round(layerX);
ctx.coords.pos.y = Math.round(layerY);
},
{target: imageCollection.inputElement}
);
/**
* 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
}

47
js/lib/layers.d.js Normal file
View file

@ -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
*/

View file

@ -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 <key>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(

View file

@ -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);

View file

@ -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,

View file

@ -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";

View file

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

View file

@ -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");

View file

@ -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;

View file

@ -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) {