diff --git a/index.html b/index.html index 47b3642..a16b2d5 100644 --- a/index.html +++ b/index.html @@ -325,6 +325,8 @@ + + diff --git a/js/lib/input.d.js b/js/lib/input.d.js index 4c5e420..07ae2b3 100644 --- a/js/lib/input.d.js +++ b/js/lib/input.d.js @@ -86,8 +86,8 @@ * * @typedef MouseCoordContext * @property {{[key: string]: MouseCoordContextDragInfo}} dragging Information about mouse button drags - * @property {{x: number, y: number}} prev Previous mouse position - * @property {{x: number, y: number}} pos Current mouse position + * @property {Point} prev Previous mouse position + * @property {Point} pos Current mouse position */ /* Here are keyboard-related types */ diff --git a/js/lib/util.js b/js/lib/util.js index 6a6e32c..83a014e 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -1,6 +1,15 @@ /** * Some type definitions before the actual code */ + +/** + * Simple Point Coordinate + * + * @typedef Point + * @property {number} x - x coordinate + * @property {number} y - y coordinate + */ + /** * Represents a simple bouding box */ @@ -22,6 +31,26 @@ class BoundingBox { this.x < x && this.y < y && x < this.x + this.w && y < this.y + this.h ); } + + /** + * Gets bounding box from two points + * + * @param {Point} start Coordinate + * @param {Point} end + */ + static fromStartEnd(start, end) { + const minx = Math.min(start.x, end.x); + const miny = Math.min(start.y, end.y); + const maxx = Math.max(start.x, end.x); + const maxy = Math.max(start.y, end.y); + + return new BoundingBox({ + x: minx, + y: miny, + w: maxx - minx, + h: maxy - miny, + }); + } } /** diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 9d71d3a..350a8d9 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -515,6 +515,16 @@ const _generate = async (endpoint, request, bb, options = {}) => { 1, true ); + const onmorehandler = mouse.listen.world.btn.middle.onclick.on( + (evn, state) => { + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + makeMore(); + state.dream_processed = true; + } + }, + 1, + true + ); const onwheelhandler = mouse.listen.world.onwheel.on( (evn, state) => { if (evn.evn.ctrlKey) return; @@ -542,9 +552,10 @@ const _generate = async (endpoint, request, bb, options = {}) => { // Stop handling inputs mouse.listen.world.onmousemove.clear(onmovehandler); - mouse.listen.world.onwheel.clear(onwheelhandler); mouse.listen.world.btn.left.onclick.clear(onclickhandler); mouse.listen.world.btn.right.onclick.clear(oncancelhandler); + mouse.listen.world.btn.middle.onclick.clear(onmorehandler); + mouse.listen.world.onwheel.clear(onwheelhandler); }; redraw(); @@ -926,87 +937,6 @@ const dream_img2img_callback = (bb, resolution, state) => { /** * Dream and img2img tools */ -const _reticle_draw = (bb, state, tool, resolution, style = {}) => { - defaultOpt(style, { - sizeTextStyle: "#FFF5", - genSizeTextStyle: "#FFF5", - toolTextStyle: "#FFF5", - reticleWidth: 1, - reticleStyle: "#FFF", - }); - - const bbvp = { - ...viewport.canvasToView(bb.x, bb.y), - w: viewport.zoom * bb.w, - h: viewport.zoom * bb.h, - }; - - uiCtx.save(); - - // draw targeting square reticle thingy cursor - uiCtx.lineWidth = style.reticleWidth; - uiCtx.strokeStyle = style.reticleStyle; - uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame - - uiCtx.font = `bold 20px Open Sans`; - - // Draw Tool Name - if (bb.h > 40) { - const xshrink = Math.min(1, (bbvp.w - 20) / uiCtx.measureText(tool).width); - - uiCtx.font = `bold ${20 * xshrink}px Open Sans`; - - uiCtx.textAlign = "left"; - uiCtx.fillStyle = style.toolTextStyle; - uiCtx.fillText(tool, bbvp.x + 10, bbvp.y + 10 + 20 * xshrink, bb.w); - } - - // Draw width and height - { - // Render Cursor Width - uiCtx.textAlign = "center"; - uiCtx.fillStyle = style.sizeTextStyle; - uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2); - const xshrink = Math.min( - 1, - (bbvp.w - 30) / uiCtx.measureText(`${bb.w}px`).width - ); - const yshrink = Math.min( - 1, - (bbvp.h - 30) / uiCtx.measureText(`${bb.h}px`).width - ); - uiCtx.font = `bold ${20 * xshrink}px Open Sans`; - uiCtx.fillText(`${bb.w}px`, 0, bbvp.h / 2 - 10 * xshrink, bb.w); - - // Render Generation Width - uiCtx.fillStyle = style.genSizeTextStyle; - uiCtx.font = `bold ${10 * xshrink}px Open Sans`; - if (bb.w !== resolution.w) - uiCtx.fillText(`${resolution.w}px`, 0, bbvp.h / 2 - 30 * xshrink, bb.h); - - // Render Cursor Height - uiCtx.rotate(-Math.PI / 2); - uiCtx.fillStyle = style.sizeTextStyle; - uiCtx.font = `bold ${20 * yshrink}px Open Sans`; - uiCtx.fillText(`${bb.h}px`, 0, bbvp.w / 2 - 10 * yshrink, bb.h); - - // Render Generation Height - uiCtx.fillStyle = style.genSizeTextStyle; - uiCtx.font = `bold ${10 * yshrink}px Open Sans`; - if (bb.h !== resolution.h) - uiCtx.fillText(`${resolution.h}px`, 0, bbvp.w / 2 - 30 * xshrink, bb.h); - - uiCtx.restore(); - } - - return () => { - uiCtx.save(); - - uiCtx.clearRect(bbvp.x - 64, bbvp.y - 64, bbvp.w + 128, bbvp.h + 128); - - uiCtx.restore(); - }; -}; /** * Generic wheel handler @@ -1045,15 +975,24 @@ const dreamTool = () => mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.onwheel.on(state.wheelcb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); + mouse.listen.world.btn.right.onclick.on(state.erasecb); + + // Select Region listeners mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); mouse.listen.world.btn.left.ondrag.on(state.dragcb); mouse.listen.world.btn.left.ondragend.on(state.dragendcb); - mouse.listen.world.btn.left.onclick.on(state.dreamcb); - mouse.listen.world.btn.right.onclick.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true); + mouse.listen.world.onwheel.on(state.swheelcb, 2, true); + mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true); + mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true); + mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true); // Clear Selection - state.selected = null; + state.selection.deselect(); // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -1068,15 +1007,22 @@ const dreamTool = () => mouse.listen.world.onmousemove.clear(state.mousemovecb); mouse.listen.world.onwheel.clear(state.wheelcb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); + mouse.listen.world.btn.right.onclick.clear(state.erasecb); + + // Clear Select Region listeners mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); mouse.listen.world.btn.left.ondrag.clear(state.dragcb); mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); - mouse.listen.world.btn.left.onclick.clear(state.dreamcb); - mouse.listen.world.btn.right.onclick.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.smousemovecb); + mouse.listen.world.onwheel.clear(state.swheelcb); + mouse.listen.world.btn.left.onclick.clear(state.sdreamcb); + mouse.listen.world.btn.right.onclick.clear(state.serasecb); + mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb); // Clear Selection - state.selected = null; + state.selection.deselect(); // Hide Mask setMask("none"); @@ -1104,32 +1050,60 @@ const dreamTool = () => ...mouse.coords.world.pos, }; - state.dragstartcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - state.selected = {start: {x, y}, now: {x, y}}; + /** + * Selection handlers + */ + const selection = _tool._draggable_selection(state); + state.dragstartcb = (evn) => selection.dragstartcb(evn); + state.dragcb = (evn) => selection.dragcb(evn); + state.dragendcb = (evn) => selection.dragendcb(evn); + state.smousemovecb = (evn, estate) => { + selection.smousemovecb(evn); + if (selection.inside) { + imageCollection.inputElement.style.cursor = "pointer"; + + estate.dream_processed = true; + } else { + imageCollection.inputElement.style.cursor = "auto"; + } }; - state.dragcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - }; - state.dragendcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - - if ( - state.selected.start.x === state.selected.now.x || - state.selected.start.y === state.selected.now.y - ) { - state.selected = null; - state.redraw(); + state.swheelcb = (evn, estate) => { + if (selection.inside) { + state.wheelcb(evn, {}); + estate.dream_processed = true; } }; + state.sdreamcb = (evn, estate) => { + if (selection.exists && !selection.inside) { + selection.deselect(); + state.redraw(); + estate.selection_processed = true; + } + if (selection.inside) { + state.dreamcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.serasecb = (evn, estate) => { + if (selection.inside) { + selection.deselect(); + state.redraw(); + estate.dream_processed = true; + } + }; + state.smiddlecb = (evn, estate) => { + if (selection.inside) { + estate.dream_processed = true; + } + }; + + state.selection = selection; + + /** + * Dream Handlers + */ state.mousemovecb = (evn) => { state.lastMouseMove = evn; @@ -1143,36 +1117,10 @@ const dreamTool = () => y += snap(evn.y, 0, 64); } - const vpc = viewport.canvasToView(x, y); + state.erasePrevReticle = _tool._cursor_draw(x, y); - // Draw current cursor location - uiCtx.lineWidth = 3; - uiCtx.strokeStyle = "#FFF5"; - - uiCtx.beginPath(); - uiCtx.moveTo(vpc.x, vpc.y + 10); - uiCtx.lineTo(vpc.x, vpc.y - 10); - uiCtx.moveTo(vpc.x + 10, vpc.y); - uiCtx.lineTo(vpc.x - 10, vpc.y); - uiCtx.stroke(); - state.eraseCursor = () => { - uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); - }; - - if (state.selected) { - const bb = new BoundingBox(); - - const minx = Math.min(state.selected.now.x, state.selected.start.x); - const miny = Math.min(state.selected.now.y, state.selected.start.y); - const maxx = Math.max(state.selected.now.x, state.selected.start.x); - const maxy = Math.max(state.selected.now.y, state.selected.start.y); - - bb.x = minx; - bb.y = miny; - bb.w = maxx - minx; - bb.h = maxy - miny; - - state.selected.bb = bb; + if (state.selection.exists) { + const bb = state.selection.bb; const style = state.cursorSize > stableDiffusionData.width @@ -1181,9 +1129,8 @@ const dreamTool = () => ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw( + state.erasePrevReticle = _tool._reticle_draw( bb, - state, "Dream", { w: Math.round( @@ -1194,6 +1141,7 @@ const dreamTool = () => ), }, { + reticleStyle: state.selection.inside ? "#F55" : "#FFF", sizeTextStyle: style, } ); @@ -1206,7 +1154,7 @@ const dreamTool = () => : state.cursorSize < stableDiffusionData.width ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw( + state.erasePrevReticle = _tool._reticle_draw( getBoundingBox( evn.x, evn.y, @@ -1214,7 +1162,6 @@ const dreamTool = () => state.cursorSize, state.snapToGrid && basePixelCount ), - state, "Dream", { w: stableDiffusionData.width, @@ -1235,9 +1182,9 @@ const dreamTool = () => _dream_onwheel(evn, state); }; state.dreamcb = (evn, estate) => { - if (estate.dream_processed) return; + if (estate.dream_processed || estate.selection_processed) return; const bb = - (state.selected && state.selected.bb) || + state.selection.bb || getBoundingBox( evn.x, evn.y, @@ -1245,16 +1192,16 @@ const dreamTool = () => state.cursorSize, state.snapToGrid && basePixelCount ); - const resolution = (state.selected && state.selected.bb) || { + const resolution = state.selection.bb || { w: stableDiffusionData.width, h: stableDiffusionData.height, }; dream_generate_callback(bb, resolution, state); - state.selected = null; + state.selection.deselect(); }; state.erasecb = (evn, estate) => { - if (state.selected) { - state.selected = null; + if (state.selection.exists) { + state.selection.deselect(); state.redraw(); return; } @@ -1361,15 +1308,22 @@ const img2imgTool = () => mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); + mouse.listen.world.btn.right.onclick.on(state.erasecb); + + // Select Region listeners mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); mouse.listen.world.btn.left.ondrag.on(state.dragcb); mouse.listen.world.btn.left.ondragend.on(state.dragendcb); - mouse.listen.world.btn.left.onclick.on(state.dreamcb); - mouse.listen.world.btn.right.onclick.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true); + mouse.listen.world.onwheel.on(state.swheelcb, 2, true); + mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true); + mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true); + mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true); // Clear Selection - state.selected = null; + state.selection.deselect(); // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -1384,15 +1338,22 @@ const img2imgTool = () => mouse.listen.world.onmousemove.clear(state.mousemovecb); mouse.listen.world.onwheel.clear(state.wheelcb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); + mouse.listen.world.btn.right.onclick.clear(state.erasecb); + + // Clear Select Region listeners mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); mouse.listen.world.btn.left.ondrag.clear(state.dragcb); mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); - mouse.listen.world.btn.left.onclick.clear(state.dreamcb); - mouse.listen.world.btn.right.onclick.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.smousemovecb); + mouse.listen.world.onwheel.clear(state.swheelcb); + mouse.listen.world.btn.left.onclick.clear(state.sdreamcb); + mouse.listen.world.btn.right.onclick.clear(state.serasecb); + mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb); // Clear Selection - state.selected = null; + state.selection.deselect(); // Hide mask setMask("none"); @@ -1424,33 +1385,59 @@ const img2imgTool = () => ...mouse.coords.world.pos, }; - state.dragstartcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - state.selected = {start: {x, y}, now: {x, y}}; + /** + * Selection handlers + */ + const selection = _tool._draggable_selection(state); + state.dragstartcb = (evn) => selection.dragstartcb(evn); + state.dragcb = (evn) => selection.dragcb(evn); + state.dragendcb = (evn) => selection.dragendcb(evn); + state.smousemovecb = (evn, estate) => { + selection.smousemovecb(evn); + if (selection.inside) { + imageCollection.inputElement.style.cursor = "pointer"; + + estate.dream_processed = true; + } else { + imageCollection.inputElement.style.cursor = "auto"; + } }; - state.dragcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - }; - - state.dragendcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - - if ( - state.selected.start.x === state.selected.now.x || - state.selected.start.y === state.selected.now.y - ) { - state.selected = null; - state.redraw(); + state.swheelcb = (evn, estate) => { + if (selection.inside) { + state.wheelcb(evn, {}); + estate.dream_processed = true; } }; + state.sdreamcb = (evn, estate) => { + if (selection.exists && !selection.inside) { + selection.deselect(); + estate.selection_processed = true; + } + if (selection.inside) { + state.dreamcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.serasecb = (evn, estate) => { + if (selection.inside) { + state.erasecb(evn, {}); + estate.dream_processed = true; + } + }; + + state.smiddlecb = (evn, estate) => { + if (selection.inside) { + estate.dream_processed = true; + } + }; + + state.selection = selection; + + /** + * Dream handlers + */ state.mousemovecb = (evn) => { state.lastMouseMove = evn; @@ -1464,46 +1451,25 @@ const img2imgTool = () => y += snap(evn.y, 0, 64); } - const vpc = viewport.canvasToView(x, y); - - // Draw current cursor location - uiCtx.lineWidth = 3; - uiCtx.strokeStyle = "#FFF5"; - - uiCtx.beginPath(); - uiCtx.moveTo(vpc.x, vpc.y + 10); - uiCtx.lineTo(vpc.x, vpc.y - 10); - uiCtx.moveTo(vpc.x + 10, vpc.y); - uiCtx.lineTo(vpc.x - 10, vpc.y); - uiCtx.stroke(); - state.eraseCursor = () => { - uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); - }; + state.erasePrevReticle = _tool._cursor_draw(x, y); // Resolution let bb = null; let request = null; - if (state.selected) { - bb = new BoundingBox(); - - const minx = Math.min(state.selected.now.x, state.selected.start.x); - const miny = Math.min(state.selected.now.y, state.selected.start.y); - const maxx = Math.max(state.selected.now.x, state.selected.start.x); - const maxy = Math.max(state.selected.now.y, state.selected.start.y); - - bb.x = minx; - bb.y = miny; - bb.w = maxx - minx; - bb.h = maxy - miny; - - state.selected.bb = bb; + if (state.selection.exists) { + bb = state.selection.bb; request = {width: bb.w, height: bb.h}; - state.erasePrevReticle = _reticle_draw( + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + state.erasePrevReticle = _tool._reticle_draw( bb, - state, "Img2Img", { w: Math.round( @@ -1514,6 +1480,7 @@ const img2imgTool = () => ), }, { + reticleStyle: state.selection.inside ? "#F55" : "#FFF", sizeTextStyle: style, } ); @@ -1537,9 +1504,8 @@ const img2imgTool = () => : state.cursorSize < stableDiffusionData.width ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw( + state.erasePrevReticle = _tool._reticle_draw( bb, - state, "Img2Img", {w: request.width, h: request.height}, { @@ -1549,9 +1515,11 @@ const img2imgTool = () => } if ( - state.selected && - (state.selected.now.x === state.selected.start.x || - state.selected.now.y === state.selected.start.y) + state.selection.exists && + (state.selection.selected.now.x === + state.selection.selected.start.x || + state.selection.selected.now.y === + state.selection.selected.start.y) ) { return; } @@ -1651,9 +1619,9 @@ const img2imgTool = () => _dream_onwheel(evn, state); }; state.dreamcb = (evn, estate) => { - if (estate.dream_processed) return; + if (estate.dream_processed || estate.selection_processed) return; const bb = - (state.selected && state.selected.bb) || + state.selection.bb || getBoundingBox( evn.x, evn.y, @@ -1661,18 +1629,18 @@ const img2imgTool = () => state.cursorSize, state.snapToGrid && basePixelCount ); - const resolution = (state.selected && state.selected.bb) || { + const resolution = state.selection.bb || { w: stableDiffusionData.width, h: stableDiffusionData.height, }; dream_img2img_callback(bb, resolution, state); - state.selected = null; + state.selection.deselect(); state.redraw(); }; state.erasecb = (evn, estate) => { if (estate.dream_processed) return; - if (state.selected) { - state.selected = null; + if (state.selection.exists) { + state.selection.deselect(); state.redraw(); return; } diff --git a/js/ui/tool/generic.js b/js/ui/tool/generic.js new file mode 100644 index 0000000..6de0388 --- /dev/null +++ b/js/ui/tool/generic.js @@ -0,0 +1,276 @@ +/** + * File to add generic rendering functions and shared utilities + */ + +const _tool = { + /** + * Draws a reticle used for image generation + * + * @param {BoundingBox} bb The bounding box of the reticle (world space) + * @param {string} tool Name of the tool to diplay + * @param {{w: number, h: number}} resolution Resolution of generation to display + * @param {object} style Styles to use for rendering the reticle + * @param {string} [style.sizeTextStyle = "#FFF5"] Style of the text for diplaying the bounding box size. + * @param {string} [style.genSizeTextStyle = "#FFF5"] Style of the text for diplaying generation size + * @param {string} [style.toolTextStyle = "#FFF5"] Style of the text for the tool name + * @param {number} [style.reticleWidth = 1] Width of the line of the reticle + * @param {string} [style.reticleStyle = "#FFF"] Style of the line of the reticle + * + * @returns A function that erases this reticle drawing + */ + _reticle_draw(bb, tool, resolution, style = {}) { + defaultOpt(style, { + sizeTextStyle: "#FFF5", + genSizeTextStyle: "#FFF5", + toolTextStyle: "#FFF5", + reticleWidth: 1, + reticleStyle: "#FFF", + }); + + const bbvp = { + ...viewport.canvasToView(bb.x, bb.y), + w: viewport.zoom * bb.w, + h: viewport.zoom * bb.h, + }; + + uiCtx.save(); + + // draw targeting square reticle thingy cursor + uiCtx.lineWidth = style.reticleWidth; + uiCtx.strokeStyle = style.reticleStyle; + uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame + + uiCtx.font = `bold 20px Open Sans`; + + // Draw Tool Name + if (bb.h > 40) { + const xshrink = Math.min( + 1, + (bbvp.w - 20) / uiCtx.measureText(tool).width + ); + + uiCtx.font = `bold ${20 * xshrink}px Open Sans`; + + uiCtx.textAlign = "left"; + uiCtx.fillStyle = style.toolTextStyle; + uiCtx.fillText(tool, bbvp.x + 10, bbvp.y + 10 + 20 * xshrink, bb.w); + } + + // Draw width and height + { + // Render Cursor Width + uiCtx.textAlign = "center"; + uiCtx.fillStyle = style.sizeTextStyle; + uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2); + const xshrink = Math.min( + 1, + (bbvp.w - 30) / uiCtx.measureText(`${bb.w}px`).width + ); + const yshrink = Math.min( + 1, + (bbvp.h - 30) / uiCtx.measureText(`${bb.h}px`).width + ); + uiCtx.font = `bold ${20 * xshrink}px Open Sans`; + uiCtx.fillText(`${bb.w}px`, 0, bbvp.h / 2 - 10 * xshrink, bb.w); + + // Render Generation Width + uiCtx.fillStyle = style.genSizeTextStyle; + uiCtx.font = `bold ${10 * xshrink}px Open Sans`; + if (bb.w !== resolution.w) + uiCtx.fillText(`${resolution.w}px`, 0, bbvp.h / 2 - 30 * xshrink, bb.h); + + // Render Cursor Height + uiCtx.rotate(-Math.PI / 2); + uiCtx.fillStyle = style.sizeTextStyle; + uiCtx.font = `bold ${20 * yshrink}px Open Sans`; + uiCtx.fillText(`${bb.h}px`, 0, bbvp.w / 2 - 10 * yshrink, bb.h); + + // Render Generation Height + uiCtx.fillStyle = style.genSizeTextStyle; + uiCtx.font = `bold ${10 * yshrink}px Open Sans`; + if (bb.h !== resolution.h) + uiCtx.fillText(`${resolution.h}px`, 0, bbvp.w / 2 - 30 * xshrink, bb.h); + + uiCtx.restore(); + } + + return () => { + uiCtx.save(); + + uiCtx.clearRect(bbvp.x - 64, bbvp.y - 64, bbvp.w + 128, bbvp.h + 128); + + uiCtx.restore(); + }; + }, + + /** + * Draws a generic crosshair cursor at the specified location + * + * @param {number} x X world coordinate of the cursor + * @param {number} y Y world coordinate of the cursor + * @param {object} style Style of the lines of the cursor + * @param {string} [style.width = 3] Line width of the lines of the cursor + * @param {string} [style.style = "#FFF5"] Stroke style of the lines of the cursor + * + * @returns A function that erases this cursor drawing + */ + _cursor_draw(x, y, style = {}) { + defaultOpt(style, { + width: 3, + style: "#FFF5", + }); + const vpc = viewport.canvasToView(x, y); + + // Draw current cursor location + uiCtx.lineWidth = style.width; + uiCtx.strokeStyle = style.style; + + uiCtx.beginPath(); + uiCtx.moveTo(vpc.x, vpc.y + 10); + uiCtx.lineTo(vpc.x, vpc.y - 10); + uiCtx.moveTo(vpc.x + 10, vpc.y); + uiCtx.lineTo(vpc.x - 10, vpc.y); + uiCtx.stroke(); + return () => { + uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); + }; + }, + + /** + * Creates generic handlers for dealing with draggable selection areas + * + * @param {object} state State of the tool + * @param {boolean} state.snapToGrid Whether the cursor should snap to the grid + * @param {() => void} [state.redraw] Function to redraw the cursor + * @returns + */ + _draggable_selection(state) { + const selection = { + _inside: false, + _dirty_bb: true, + _cached_bb: null, + _selected: null, + + /** + * If the cursor is cursor is currently inside the selection + */ + get inside() { + return this._inside; + }, + + /** + * Get intermediate selection object + */ + get selected() { + return this._selected; + }, + + /** + * If the selection exists + */ + get exists() { + return !!this._selected; + }, + + /** + * Gets the selection bounding box + */ + get bb() { + if (this._dirty_bb && this._selected) { + this._cached_bb = BoundingBox.fromStartEnd( + this._selected.start, + this._selected.now + ); + this._dirty_bb = false; + } + return this._selected && this._cached_bb; + }, + + /** + * When the cursor enters the selection + */ + onenter: new Observer(), + + /** + * When the cursor leaves the selection + */ + onleave: new Observer(), + + // Utility methods + deselect() { + if (this.inside) { + this._inside = false; + this.onleave.emit({evn: null}); + } + this._selected = null; + }, + + // Dragging handlers + /** + * Drag start event handler + * + * @param {Point} evn Drag start event + */ + dragstartcb(evn) { + const x = state.snapToGrid ? evn.ix + snap(evn.ix, 0, 64) : evn.ix; + const y = state.snapToGrid ? evn.iy + snap(evn.iy, 0, 64) : evn.iy; + this._selected = {start: {x, y}, now: {x, y}}; + this._dirty_bb = true; + }, + /** + * Drag event handler + * + * @param {Point} evn Drag event + */ + dragcb(evn) { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + if (x !== this._selected.now.x || y !== this._selected.now.y) { + this._selected.now = {x, y}; + this._dirty_bb = true; + } + }, + /** + * Drag end event handler + * + * @param {Point} evn Drag end event + */ + dragendcb(evn) { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + this._selected.now = {x, y}; + this._dirty_bb = true; + + if ( + this._selected.start.x === this._selected.now.x || + this._selected.start.y === this._selected.now.y + ) { + this.deselect(); + } + }, + + /** + * Mouse move event handler + * + * @param {Point} evn Mouse move event + */ + smousemovecb(evn) { + if (!this._selected || !this.bb.contains(evn.x, evn.y)) { + if (this.inside) { + this._inside = false; + this.onleave.emit({evn}); + } + } else { + if (!this.inside) { + this._inside = true; + this.onenter.emit({evn}); + } + } + }, + }; + + return selection; + }, +};