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