openOutpaint/js/initalize/layers.populate.js
Victor Seiji Hariki fc8e6fb557 Quick fix for zoom to cursor and brush erase blur
Quick fix to some issues pointed out by @raivshard at #177

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
2023-01-22 09:18:47 -03:00

387 lines
10 KiB
JavaScript

// Layering
const imageCollection = layers.registerCollection(
"image",
{
w: parseInt(
(localStorage &&
localStorage.getItem("openoutpaint/settings.canvas-width")) ||
2048
),
h: parseInt(
(localStorage &&
localStorage.getItem("openoutpaint/settings.canvas-height")) ||
2048
),
},
{
name: "Image Layers",
}
);
const bgLayer = imageCollection.registerLayer("bg", {
name: "Background",
category: "background",
});
bgLayer.canvas.classList.add("pixelated");
const imgLayer = imageCollection.registerLayer("image", {
name: "Image",
category: "image",
ctxOptions: {desynchronized: true},
});
const maskPaintLayer = imageCollection.registerLayer("mask", {
name: "Mask Paint",
category: "mask",
ctxOptions: {desynchronized: true},
});
const ovLayer = imageCollection.registerLayer("overlay", {
name: "Overlay",
category: "display",
});
const debugLayer = imageCollection.registerLayer("debug", {
name: "Debug Layer",
category: "display",
});
const imgCanvas = imgLayer.canvas; // where dreams go
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;
/* WIP: Most cursors shouldn't need a zoomable canvas */
/** @type {HTMLCanvasElement} */
const uiCanvas = document.getElementById("layer-overlay"); // where mouse cursor renders
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("openoutpaint/expand-size") || 1024;
expandSize = parseInt(expandSize, 10);
const askSize = (e) => {
if (e.ctrlKey) return expandSize;
const by = prompt("How much do you want to expand by?", expandSize);
if (!by) return null;
else {
const len = parseInt(by, 10);
localStorage.setItem("openoutpaint/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", (e) => {
let size = null;
if ((size = askSize(e))) {
imageCollection.expand(size, 0, 0, 0);
bgLayer.canvas.style.backgroundPosition = `${-snap(
imageCollection.origin.x,
0,
config.gridSize * 2
)}px ${-snap(imageCollection.origin.y, 0, config.gridSize * 2)}px`;
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", (e) => {
let size = null;
if ((size = askSize(e))) {
imageCollection.expand(0, 0, size, 0);
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", (e) => {
let size = null;
if ((size = askSize(e))) {
imageCollection.expand(0, size, 0, 0);
bgLayer.canvas.style.backgroundPosition = `${-snap(
imageCollection.origin.x,
0,
config.gridSize * 2
)}px ${-snap(imageCollection.origin.y, 0, config.gridSize * 2)}px`;
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", (e) => {
let size = null;
if ((size = askSize(e))) {
imageCollection.expand(0, 0, 0, size);
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
// 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
*
* cx and cy are the viewport's world coordinates.
*
* The transform() function does some transforms and writes them to the
* provided element.
*/
class Viewport {
cx = 0;
cy = 0;
zoom = 1;
/**
* Gets viewport width in canvas coordinates
*/
get w() {
return window.innerWidth * this.zoom;
}
/**
* Gets viewport height in canvas coordinates
*/
get h() {
return window.innerHeight * this.zoom;
}
constructor(x, y) {
this.x = x;
this.y = y;
}
get v2c() {
const m = new DOMMatrix();
m.translateSelf(-this.w / 2, -this.h / 2);
m.translateSelf(this.cx, this.cy);
m.scaleSelf(this.zoom);
return m;
}
get c2v() {
return this.v2c.invertSelf();
}
viewToCanvas(x, y) {
if (x.x !== undefined) return this.v2c.transformPoint(x);
return this.v2c.transformPoint({x, y});
}
canvasToView(x, y) {
if (x.x !== undefined) return this.c2v.transformPoint(x);
return this.c2v.transformPoint({x, y});
}
/**
* Apply transformation
*
* @param {HTMLElement} el Element to apply CSS transform to
*/
transform(el) {
el.style.transformOrigin = "0px 0px";
el.style.transform = this.c2v;
}
}
const viewport = new Viewport(0, 0);
viewport.cx = imageCollection.size.w / 2;
viewport.cy = imageCollection.size.h / 2;
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,
validate: (evn) => {
if ((!global.hasActiveInput && !evn.ctrlKey) || evn.type === "mousemove")
return true;
return false;
},
}
);
mouse.registerContext(
"camera",
(evn, ctx) => {
ctx.coords.prev.x = ctx.coords.pos.x;
ctx.coords.prev.y = ctx.coords.pos.y;
// Set coords
ctx.coords.pos.x = evn.x;
ctx.coords.pos.y = evn.y;
},
{
validate: (evn) => {
return !!evn.ctrlKey;
},
}
);
// Redraw on active input state change
(() => {
mouse.listen.window.onany.on((evn) => {
const activeInput = DOM.hasActiveInput();
if (global.hasActiveInput !== activeInput) {
global.hasActiveInput = activeInput;
toolbar.currentTool &&
toolbar.currentTool.state.redraw &&
toolbar.currentTool.state.redraw();
}
});
})();
mouse.listen.camera.onwheel.on((evn) => {
evn.evn.preventDefault();
// Get cursor world position
const wcursor = viewport.viewToCanvas(evn.x, evn.y);
// Get viewport center
const wcx = viewport.cx;
const wcy = viewport.cy;
// Apply zoom
viewport.zoom *= 1 + evn.delta * 0.0002;
// Get cursor new world position
const nwcursor = viewport.viewToCanvas(evn.x, evn.y);
// Apply normal zoom (center of viewport)
viewport.cx = wcx;
viewport.cy = wcy;
// Move viewport to keep cursor in same location
viewport.cx += wcursor.x - nwcursor.x;
viewport.cy += wcursor.y - nwcursor.y;
viewport.transform(imageCollection.element);
toolbar._current_tool.redrawui && toolbar._current_tool.redrawui();
});
const cameraPaintStart = (evn) => {
worldInit = {x: viewport.cx, y: viewport.cy};
};
const cameraPaint = (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 - 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
}
viewport.transform(imageCollection.element);
toolbar._current_tool.state.redrawui &&
toolbar._current_tool.state.redrawui();
if (global.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();
}
};
const cameraPaintEnd = (evn) => {
worldInit = null;
};
mouse.listen.camera.btn.middle.onpaintstart.on(cameraPaintStart);
mouse.listen.camera.btn.left.onpaintstart.on(cameraPaintStart);
mouse.listen.camera.btn.middle.onpaint.on(cameraPaint);
mouse.listen.camera.btn.left.onpaint.on(cameraPaint);
mouse.listen.window.btn.middle.onpaintend.on(cameraPaintEnd);
mouse.listen.window.btn.left.onpaintend.on(cameraPaintEnd);
window.addEventListener("resize", () => {
viewport.transform(imageCollection.element);
uiCanvas.width = uiCanvas.clientWidth;
uiCanvas.height = uiCanvas.clientHeight;
});