diff --git a/css/icons.css b/css/icons.css index e808b3b..d1c89d7 100644 --- a/css/icons.css +++ b/css/icons.css @@ -17,7 +17,9 @@ bottom: 15%; mask-size: contain; + -webkit-mask-size: contain; mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; max-height: 70%; aspect-ratio: 1; diff --git a/css/layers.css b/css/layers.css index a4f2ee7..434e77e 100644 --- a/css/layers.css +++ b/css/layers.css @@ -25,6 +25,7 @@ .layer-render-target .collection { position: absolute; + transform-origin: 0px 0px; } .layer-render-target .collection > .collection-input-overlay { @@ -45,7 +46,7 @@ right: 0; } -#layer-overlay { +.overlay-canvas { position: fixed; top: 0; diff --git a/css/ui/generic.css b/css/ui/generic.css index 801e2ba..09eb6ea 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -231,6 +231,7 @@ div.autocomplete > .autocomplete-list { } div.autocomplete > .autocomplete-list > .autocomplete-option { + position: relative; cursor: pointer; overflow: hidden; diff --git a/css/ui/layers.css b/css/ui/layers.css index c433b9c..385498d 100644 --- a/css/ui/layers.css +++ b/css/ui/layers.css @@ -158,7 +158,9 @@ background-color: #293d3d77; + -webkit-mask-image: url("../../res/icons/chevron-up.svg"); mask-image: url("../../res/icons/chevron-up.svg"); + -webkit-mask-size: contain; mask-size: contain; width: 60px; diff --git a/index.html b/index.html index af89d74..c5fbb50 100644 --- a/index.html +++ b/index.html @@ -5,15 +5,15 @@ openOutpaint 🐠 - + - + - + - + @@ -49,6 +49,7 @@ +
@@ -319,7 +320,8 @@
- + +
@@ -339,21 +341,21 @@ - + - + - + @@ -368,20 +370,20 @@ - + - + diff --git a/js/config.js b/js/config.js index a6f7ba6..5f55a75 100644 --- a/js/config.js +++ b/js/config.js @@ -5,9 +5,37 @@ */ const config = makeReadOnly( { + // Grid Size + gridSize: 64, + // Scroll Tick Limit (How much must scroll to reach next tick) wheelTickSize: 50, + /** Select Tool */ + // Handle Draw Size + handleDrawSize: 12, + // Handle Draw Hover Scale + handleDrawHoverScale: 1.3, + // Handle Detect Size + handleDetectSize: 20, + // Rotate Handle Distance (from selection) + rotateHandleDistance: 32, + + // Rotation Snapping Distance + rotationSnappingDistance: (10 * Math.PI) / 180, + // Rotation Snapping Angles + rotationSnappingAngles: [ + (-Math.PI * 4) / 4, + (-Math.PI * 3) / 4, + (-Math.PI * 2) / 4, + (-Math.PI * 1) / 4, + 0, + (Math.PI * 1) / 4, + (Math.PI * 2) / 4, + (Math.PI * 3) / 4, + (Math.PI * 4) / 4, + ], + // Endpoint api: makeReadOnly({path: "/sdapi/v1/"}), }, diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 66a1a4b..296d087 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -160,61 +160,72 @@ debugLayer.hide(); // Hidden by default * 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, scaled to zoom level. - * _x and _y are actual coordinates in the DOM space + * cx and cy are the viewport's world coordinates. * * The transform() function does some transforms and writes them to the * provided element. */ -const viewport = { - get cx() { - return this._x * this.zoom; - }, +class Viewport { + cx = 0; + cy = 0; - set cx(v) { - return (this._x = v / this.zoom); - }, - _x: 0, - get cy() { - return this._y * this.zoom; - }, - set cy(v) { - return (this._y = v / this.zoom); - }, - _y: 0, - zoom: 1, - rotation: 0, + zoom = 1; + + /** + * Gets viewport width in canvas coordinates + */ get w() { - return (window.innerWidth * 1) / this.zoom; - }, + return window.innerWidth * this.zoom; + } + + /** + * Gets viewport height in canvas coordinates + */ get h() { - return (window.innerHeight * 1) / this.zoom; - }, + 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) { - return { - x: this.cx + this.w * (x / window.innerWidth - 0.5), - y: this.cy + this.h * (y / window.innerHeight - 0.5), - }; - }, + if (x.x !== undefined) return this.v2c.transformPoint(x); + return this.v2c.transformPoint({x, y}); + } + canvasToView(x, y) { - return { - x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2, - y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2, - }; - }, + 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 = `${this.cx}px ${this.cy}px`; - el.style.transform = `scale(${this.zoom}) translate(${-( - this._x - - this.w / 2 - )}px, ${-(this._y - this.h / 2)}px)`; - }, -}; + 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; @@ -296,7 +307,7 @@ mouse.listen.camera.onwheel.on((evn) => { const pcy = viewport.cy; // Apply zoom - viewport.zoom *= 1 - evn.delta * 0.0002; + viewport.zoom *= 1 + evn.delta * 0.0002; // Apply normal zoom (center of viewport) viewport.cx = pcx; @@ -305,13 +316,13 @@ mouse.listen.camera.onwheel.on((evn) => { viewport.transform(imageCollection.element); // Calculate new viewport center and move - const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y); - viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x); - viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y); + //const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y); + //viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x); + //viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y); - viewport.transform(imageCollection.element); + //viewport.transform(imageCollection.element); - toolbar.currentTool.redraw(); + toolbar._current_tool.redrawui && toolbar._current_tool.redrawui(); }); const cameraPaintStart = (evn) => { @@ -320,8 +331,8 @@ const cameraPaintStart = (evn) => { 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; + 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( @@ -337,6 +348,9 @@ const cameraPaint = (evn) => { } 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"; diff --git a/js/lib/toolbar.js b/js/lib/toolbar.js index 4a82f0f..6706410 100644 --- a/js/lib/toolbar.js +++ b/js/lib/toolbar.js @@ -90,14 +90,20 @@ const toolbar = { name: toolname, enabled: false, _element: null, - state: {}, + state: { + redrawui: () => tool.state.redraw && tool.state.redraw(), + }, options, /** * If the tool has a redraw() function in its state, then run it */ redraw: () => { + tool.state.redrawui && tool.state.redrawui(); tool.state.redraw && tool.state.redraw(); }, + redrawui: () => { + tool.state.redrawui && tool.state.redrawui(); + }, enable: (opt = null) => { if (toolbar._locked) return; diff --git a/js/lib/util.js b/js/lib/util.js index 850b346..06ac99a 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -32,6 +32,31 @@ class BoundingBox { w = 0; h = 0; + /** @type {Point} */ + get tl() { + return {x: this.x, y: this.y}; + } + + /** @type {Point} */ + get tr() { + return {x: this.x + this.w, y: this.y}; + } + + /** @type {Point} */ + get bl() { + return {x: this.x, y: this.y + this.h}; + } + + /** @type {Point} */ + get br() { + return {x: this.x + this.w, y: this.y + this.h}; + } + + /** @type {Point} */ + get center() { + return {x: this.x + this.w / 2, y: this.y + this.h / 2}; + } + constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) { this.x = x; this.y = y; @@ -64,6 +89,18 @@ class BoundingBox { h: maxy - miny, }); } + + /** + * Returns a transformed bounding box (using top-left, bottom-right points) + * + * @param {DOMMatrix} transform Transformation matrix to transform points + */ + transform(transform) { + return BoundingBox.fromStartEnd( + transform.transformPoint({x: this.x, y: this.y}), + transform.transformPoint({x: this.x + this.w, y: this.y + this.h}) + ); + } } /** diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index dc1d190..46e82f4 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -176,7 +176,7 @@ const colorBrushTool = () => uiCtx.arc( vcp.x, vcp.y, - (state.eyedropper ? 50 : state.brushSize / 2) * viewport.zoom, + (state.eyedropper ? 50 : state.brushSize / 2) / viewport.zoom, 0, 2 * Math.PI, true @@ -197,7 +197,7 @@ const colorBrushTool = () => uiCtx.arc( vcp.x, vcp.y, - (state.brushSize / 2) * viewport.zoom, + state.brushSize / (2 * viewport.zoom), 0, 2 * Math.PI, true diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index aed0098..bce7e3b 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -1241,10 +1241,16 @@ const _dream_onwheel = (evn, state) => { return; } - // A simple but (I hope) effective fix for mouse wheel behavior - _dream_wheel_accum += evn.delta; + let delta = evn.delta; + if (evn.evn.shiftKey) delta *= 0.01; - if (Math.abs(_dream_wheel_accum) > config.wheelTickSize) { + // A simple but (I hope) effective fix for mouse wheel behavior + _dream_wheel_accum += delta; + + if ( + !evn.evn.shiftKey && + Math.abs(_dream_wheel_accum) > config.wheelTickSize + ) { // Snap to next or previous position const v = state.cursorSize - @@ -1253,6 +1259,12 @@ const _dream_onwheel = (evn, state) => { state.cursorSize = state.setCursorSize(v + snap(v, 0, 128)); state.mousemovecb(evn); + _dream_wheel_accum = 0; // Zero accumulation + } else if (evn.evn.shiftKey && Math.abs(_dream_wheel_accum) >= 1) { + const v = state.cursorSize - _dream_wheel_accum; + state.cursorSize = state.setCursorSize(v); + state.mousemovecb(evn); + _dream_wheel_accum = 0; // Zero accumulation } }; @@ -1942,11 +1954,10 @@ const img2imgTool = () => return; } - const bbvp = { - ...viewport.canvasToView(bb.x, bb.y), - w: viewport.zoom * bb.w, - h: viewport.zoom * bb.h, - }; + const bbvp = BoundingBox.fromStartEnd( + viewport.canvasToView(bb.tl), + viewport.canvasToView(bb.br) + ); // For displaying border mask const bbCanvas = document.createElement("canvas"); diff --git a/js/ui/tool/generic.js b/js/ui/tool/generic.js index c9be07f..04073b8 100644 --- a/js/ui/tool/generic.js +++ b/js/ui/tool/generic.js @@ -27,11 +27,7 @@ const _tool = { reticleStyle: global.hasActiveInput ? "#BBF" : "#FFF", }); - const bbvp = { - ...viewport.canvasToView(bb.x, bb.y), - w: viewport.zoom * bb.w, - h: viewport.zoom * bb.h, - }; + const bbvp = bb.transform(viewport.c2v); uiCtx.save(); @@ -174,6 +170,8 @@ const _tool = { /** * Gets the selection bounding box + * + * @returns {BoundingBox} */ get bb() { if (this._dirty_bb && this._selected) { @@ -273,4 +271,389 @@ const _tool = { return selection; }, + + /** + * Processes cursor position + * + * @param {Point} wpoint World coordinate of the cursor + * @param {boolean} snapToGrid Snap to grid + */ + _process_cursor(wpoint, snapToGrid) { + // Get cursor positions + let x = wpoint.x; + let y = wpoint.y; + let sx = x; + let sy = y; + + if (snapToGrid) { + sx += snap(x, 0, config.gridSize); + sy += snap(y, 0, config.gridSize); + } + + const vpc = viewport.canvasToView(x, y); + const vpsc = viewport.canvasToView(sx, sy); + + return { + // World Coordinates + x, + y, + sx, + sy, + + // Viewport Coordinates + vpx: vpc.x, + vpy: vpc.y, + vpsx: vpsc.x, + vpsy: vpsc.y, + }; + }, + + /** + * Represents a marquee selection with an image + */ + MarqueeSelection: class { + /** @type {HTMLCanvasElement} */ + canvas; + + _dirty = false; + _position = {x: 0, y: 0}; + /** + * @type {Point} + */ + get position() { + return this._position; + } + set position(v) { + this._dirty = true; + this._position = v; + } + + _scale = {x: 1, y: 1}; + /** + * @type {Point} + */ + get scale() { + return this._scale; + } + set scale(v) { + if (v.x === 0 || v.y === 0) return; + this._dirty = true; + this._scale = v; + } + + _rotation = 0; + get rotation() { + return this._rotation; + } + set rotation(v) { + this._dirty = true; + this._rotation = v; + } + + /** + * @param {HTMLCanvasElement} canvas Selected image canvas + * @param {Point} position Initial position of the selection + */ + constructor(canvas, position = {x: 0, y: 0}) { + this.canvas = canvas; + this.position = position; + } + + /** @type {DOMMatrix} */ + _rtmatrix = null; + get rtmatrix() { + if (!this._rtmatrix || this._dirty) { + const m = new DOMMatrix(); + + m.translateSelf(this.position.x, this.position.y); + m.rotateSelf((this.rotation * 180) / Math.PI); + + this._rtmatrix = m; + } + + return this._rtmatrix; + } + + /** @type {DOMMatrix} */ + _matrix = null; + get matrix() { + if (!this._matrix || this._dirty) { + this._matrix = this.rtmatrix.scaleSelf(this.scale.x, this.scale.y); + } + return this._matrix; + } + + /** + * If the main marquee box contains a given point + * + * @param {number} x X coordinate of the point + * @param {number} y Y coordinate of the point + */ + contains(x, y) { + const p = this.matrix.invertSelf().transformPoint({x, y}); + + return ( + Math.abs(p.x) < this.canvas.width / 2 && + Math.abs(p.y) < this.canvas.height / 2 + ); + } + + hoveringRotateHandle(x, y, scale = 1) { + const localc = this.rtmatrix.inverse().transformPoint({x, y}); + const localrh = { + x: 0, + y: + (-this.scale.y * this.canvas.height) / 2 - + config.rotateHandleDistance * scale, + }; + + const dx = Math.abs(localc.x - localrh.x); + const dy = Math.abs(localc.y - localrh.y); + + return ( + dx * dx + dy * dy < + (scale * scale * config.handleDetectSize * config.handleDetectSize) / 4 + ); + } + + hoveringHandle(x, y, scale = 1) { + const localbb = new BoundingBox({ + x: (this.scale.x * -this.canvas.width) / 2, + y: (this.scale.y * -this.canvas.height) / 2, + w: this.canvas.width * this.scale.x, + h: this.canvas.height * this.scale.y, + }); + + const localc = this.rtmatrix.inverse().transformPoint({x, y}); + const ontl = + Math.max( + Math.abs(localc.x - localbb.tl.x), + Math.abs(localc.y - localbb.tl.y) + ) < + (config.handleDetectSize / 2) * scale; + const ontr = + Math.max( + Math.abs(localc.x - localbb.tr.x), + Math.abs(localc.y - localbb.tr.y) + ) < + (config.handleDetectSize / 2) * scale; + const onbl = + Math.max( + Math.abs(localc.x - localbb.bl.x), + Math.abs(localc.y - localbb.bl.y) + ) < + (config.handleDetectSize / 2) * scale; + const onbr = + Math.max( + Math.abs(localc.x - localbb.br.x), + Math.abs(localc.y - localbb.br.y) + ) < + (config.handleDetectSize / 2) * scale; + + return {onHandle: ontl || ontr || onbl || onbr, ontl, ontr, onbl, onbr}; + } + + hoveringBox(x, y) { + const localbb = new BoundingBox({ + x: -this.canvas.width / 2, + y: -this.canvas.height / 2, + w: this.canvas.width, + h: this.canvas.height, + }); + + const localc = this.matrix.inverse().transformPoint({x, y}); + + return ( + !this.hoveringHandle(x, y).onHandle && + localbb.contains(localc.x, localc.y) + ); + } + + /** + * Draws the marquee selector box + * + * @param {CanvasRenderingContext2D} context A context for rendering the box to + * @param {Point} cursor Cursor position + * @param {DOMMatrix} transform A transformation matrix to transform the position by + */ + drawBox(context, cursor, transform = new DOMMatrix()) { + const drawscale = + 1 / Math.sqrt(transform.a * transform.a + transform.b * transform.b); + const m = transform.multiply(this.matrix); + + context.save(); + + const localbb = new BoundingBox({ + x: -this.canvas.width / 2, + y: -this.canvas.height / 2, + w: this.canvas.width, + h: this.canvas.height, + }); + + // Line Style + context.strokeStyle = "#FFF"; + context.lineWidth = 2; + + const tl = m.transformPoint(localbb.tl); + const tr = m.transformPoint(localbb.tr); + const bl = m.transformPoint(localbb.bl); + const br = m.transformPoint(localbb.br); + + const bbc = m.transformPoint({x: 0, y: 0}); + + context.beginPath(); + context.arc(bbc.x, bbc.y, 5, 0, Math.PI * 2); + context.stroke(); + + context.setLineDash([4, 2]); + + // Draw main rectangle + context.beginPath(); + context.moveTo(tl.x, tl.y); + context.lineTo(tr.x, tr.y); + context.lineTo(br.x, br.y); + context.lineTo(bl.x, bl.y); + context.lineTo(tl.x, tl.y); + context.stroke(); + + // Draw rotation handle + context.setLineDash([]); + + const hm = new DOMMatrix().rotateSelf((this.rotation * 180) / Math.PI); + const tm = m.transformPoint({x: 0, y: -this.canvas.height / 2}); + const rho = hm.transformPoint({x: 0, y: -config.rotateHandleDistance}); + const rh = {x: tm.x + rho.x, y: tm.y + rho.y}; + + let handleRadius = config.handleDrawSize / 2; + if (this.hoveringRotateHandle(cursor.x, cursor.y, drawscale)) + handleRadius *= config.handleDrawHoverScale; + + context.beginPath(); + context.moveTo(tm.x, tm.y); + context.lineTo(rh.x, rh.y); + context.stroke(); + + context.beginPath(); + context.arc(rh.x, rh.y, handleRadius, 0, 2 * Math.PI); + context.stroke(); + + // Draw handles + const drawHandle = (pt, hover) => { + let hsz = config.handleDrawSize / 2; + if (hover) hsz *= config.handleDrawHoverScale; + + const htl = hm.transformPoint({x: -hsz, y: -hsz}); + const htr = hm.transformPoint({x: hsz, y: -hsz}); + const hbr = hm.transformPoint({x: hsz, y: hsz}); + const hbl = hm.transformPoint({x: -hsz, y: hsz}); + + context.beginPath(); + context.moveTo(htl.x + pt.x, htl.y + pt.y); + context.lineTo(htr.x + pt.x, htr.y + pt.y); + context.lineTo(hbr.x + pt.x, hbr.y + pt.y); + context.lineTo(hbl.x + pt.x, hbl.y + pt.y); + context.lineTo(htl.x + pt.x, htl.y + pt.y); + context.stroke(); + }; + + context.strokeStyle = "#FFF"; + context.lineWidth = 2; + context.setLineDash([]); + + const {ontl, ontr, onbl, onbr} = this.hoveringHandle( + cursor.x, + cursor.y, + drawscale + ); + + drawHandle(tl, ontl); + drawHandle(tr, ontr); + drawHandle(bl, onbl); + drawHandle(br, onbr); + + context.restore(); + + return () => { + const border = config.handleDrawSize * config.handleDrawHoverScale; + + const minx = Math.min(tl.x, tr.x, bl.x, br.x, rh.x) - border; + const maxx = Math.max(tl.x, tr.x, bl.x, br.x, rh.x) + border; + const miny = Math.min(tl.y, tr.y, bl.y, br.y, rh.y) - border; + const maxy = Math.max(tl.y, tr.y, bl.y, br.y, rh.y) + border; + + context.clearRect(minx, miny, maxx - minx, maxy - miny); + }; + } + + /** + * Draws the selected image + * + * @param {CanvasRenderingContext2D} context A context for rendering the image to + * @param {CanvasRenderingContext2D} peekctx A context for rendering the layer peeking to + * @param {object} options + * @param {DOMMatrix} options.transform A transformation matrix to transform the position by + * @param {number} options.opacity Opacity of the peek display + */ + drawImage(context, peekctx, options = {}) { + defaultOpt(options, { + transform: new DOMMatrix(), + opacity: 0.4, + }); + + context.save(); + peekctx.save(); + + const m = options.transform.multiply(this.matrix); + + // Draw image + context.setTransform(m); + context.drawImage( + this.canvas, + -this.canvas.width / 2, + -this.canvas.height / 2, + this.canvas.width, + this.canvas.height + ); + + // Draw peek + peekctx.filter = `opacity(${options.opacity * 100}%)`; + peekctx.setTransform(m); + peekctx.drawImage( + this.canvas, + -this.canvas.width / 2, + -this.canvas.height / 2, + this.canvas.width, + this.canvas.height + ); + + peekctx.restore(); + context.restore(); + + return () => { + // Here we only save transform for performance + const pt = context.getTransform(); + const ppt = context.getTransform(); + + context.setTransform(m); + peekctx.setTransform(m); + + context.clearRect( + -this.canvas.width / 2 - 10, + -this.canvas.height / 2 - 10, + this.canvas.width + 20, + this.canvas.height + 20 + ); + + peekctx.clearRect( + -this.canvas.width / 2 - 10, + -this.canvas.height / 2 - 10, + this.canvas.width + 20, + this.canvas.height + 20 + ); + + context.setTransform(pt); + peekctx.setTransform(ppt); + }; + } + }, }; diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 11eb700..794ff7a 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -11,6 +11,7 @@ const selectTransformTool = () => mouse.listen.world.onmousemove.on(state.movecb); mouse.listen.world.btn.left.onclick.on(state.clickcb); 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); // Canvas right mouse handler @@ -29,12 +30,19 @@ const selectTransformTool = () => keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb); state.selected = null; + + // Register Layer + state.originalDisplayLayer = imageCollection.registerLayer(null, { + after: uil.layer, + category: "select-display", + }); }, (state, opt) => { // Clear all those listeners and shortcuts we set up mouse.listen.world.onmousemove.clear(state.movecb); mouse.listen.world.btn.left.onclick.clear(state.clickcb); 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.right.onclick.clear(state.cancelcb); @@ -55,6 +63,10 @@ const selectTransformTool = () => // Clears overlay imageCollection.inputElement.style.cursor = "auto"; + + // Delete Layer + imageCollection.deleteLayer(state.originalDisplayLayer); + state.originalDisplayLayer = null; }, { init: (state) => { @@ -69,7 +81,6 @@ const selectTransformTool = () => state.selectionPeekOpacity = 40; state.original = null; - state.dragging = null; state._selected = null; Object.defineProperty(state, "selected", { get: () => state._selected, @@ -80,7 +91,6 @@ const selectTransformTool = () => return (state._selected = v); }, }); - state.moving = null; // Some things to easy request for a redraw state.lastMouseTarget = null; @@ -97,423 +107,334 @@ const selectTransformTool = () => } }; + /** @type {{selected: Point, offset: Point} | null} */ + let moving = null; + /** @type {{handle: Point} | null} */ + let scaling = null; + let rotating = false; + // Clears selection and make things right state.reset = (erase = false) => { if (state.selected && !erase) - state.originalLayer.ctx.drawImage( - state.original.image, + state.original.layer.ctx.drawImage( + state.selected.canvas, state.original.x, state.original.y ); if (state.originalDisplayLayer) { - imageCollection.deleteLayer(state.originalDisplayLayer); - state.originalDisplayLayer = null; + state.originalDisplayLayer.clear(); } if (state.dragging) state.dragging = null; else state.selected = null; + state.rotation = 0; + state.original = null; + moving = null; + scaling = null; + rotating = null; + state.redraw(); }; - // Selection bounding box object. Has some witchery to deal with handles. - const selectionBB = (x1, y1, x2, y2) => { - x1 = Math.round(x1); - y1 = Math.round(y1); - x2 = Math.round(x2); - y2 = Math.round(y2); - return { - original: { - x: Math.min(x1, x2), - y: Math.min(y1, y2), - w: Math.abs(x1 - x2), - h: Math.abs(y1 - y2), - }, - x: Math.min(x1, x2), - y: Math.min(y1, y2), - w: Math.abs(x1 - x2), - h: Math.abs(y1 - y2), - updateOriginal() { - this.original.x = this.x; - this.original.y = this.y; - this.original.w = this.w; - this.original.h = this.h; - }, - contains(x, y) { - return ( - this.x <= x && - x <= this.x + this.w && - this.y <= y && - y <= this.y + this.h - ); - }, - handles() { - const _createHandle = (x, y, originOffset = null) => { - return { - x, - y, - scaleTo: (tx, ty, keepAspectRatio = true) => { - const origin = { - x: this.original.x + this.original.w / 2, - y: this.original.y + this.original.h / 2, - }; - let nx = tx; - let ny = ty; + // Selection Handlers + const selection = _tool._draggable_selection(state); - let xRatio = (nx - origin.x) / (x - origin.x); - let yRatio = (ny - origin.y) / (y - origin.y); - if (keepAspectRatio) - xRatio = yRatio = Math.min(xRatio, yRatio); + // UI Erasers + let eraseSelectedBox = () => null; + let eraseSelectedImage = () => null; + let eraseCursor = () => null; + let eraseSelection = () => null; - if (Number.isFinite(xRatio)) { - let left = this.original.x; - let right = this.original.x + this.original.w; + // Redraw UI + state.redrawui = () => { + // Get cursor positions + const {x, y, sx, sy} = _tool._process_cursor( + state.lastMouseMove, + state.snapToGrid + ); - left = (left - origin.x) * xRatio + origin.x; - right = (right - origin.x) * xRatio + origin.x; - - this.x = left; - this.w = right - left; - } - - if (Number.isFinite(yRatio)) { - let top = this.original.y; - let bottom = this.original.y + this.original.h; - - top = (top - origin.y) * yRatio + origin.y; - bottom = (bottom - origin.y) * yRatio + origin.y; - - this.y = top; - this.h = bottom - top; - } - }, - }; - }; - - const size = viewport.zoom * 10; - return [ - _createHandle(this.x, this.y, size), - _createHandle(this.x + this.w, this.y, size), - _createHandle(this.x, this.y + this.h, size), - _createHandle(this.x + this.w, this.y + this.h, size), - ]; - }, - }; - }; - - // Mouse move handler. As always, also renders cursor - state.movecb = (evn) => { - ovLayer.clear(); - uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); - state.erasePrevCursor && state.erasePrevCursor(); - imageCollection.inputElement.style.cursor = "auto"; - state.lastMouseTarget = evn.target; - state.lastMouseMove = evn; - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, 0, 64); - y += snap(evn.y, 0, 64); - } - - const vpc = viewport.canvasToView(x, y); - - uiCtx.save(); - - // Update scale - if (state.scaling) { - state.scaling.scaleTo(x, y, state.keepAspectRatio); - } - - // Update position - if (state.moving) { - state.selected.x = Math.round(x - state.moving.offset.x); - state.selected.y = Math.round(y - state.moving.offset.y); - state.selected.updateOriginal(); - } - - // Draw dragging box - if (state.dragging) { - uiCtx.setLineDash([2, 2]); - uiCtx.lineWidth = 1; - uiCtx.strokeStyle = "#FFF"; - - const ix = state.dragging.ix; - const iy = state.dragging.iy; - - const bb = selectionBB(ix, iy, x, y); - - const bbvp = { - ...viewport.canvasToView(bb.x, bb.y), - w: viewport.zoom * bb.w, - h: viewport.zoom * bb.h, - }; - - uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); - uiCtx.setLineDash([]); - } + eraseSelectedBox(); if (state.selected) { - ovCtx.lineWidth = 1; - ovCtx.strokeStyle = "#FFF"; - - const bb = { - x: state.selected.x, - y: state.selected.y, - w: state.selected.w, - h: state.selected.h, - }; - - const bbvp = { - ...viewport.canvasToView(bb.x, bb.y), - w: viewport.zoom * bb.w, - h: viewport.zoom * bb.h, - }; - - // Draw Image - ovCtx.save(); - ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`; - ovCtx.drawImage( - state.selected.image, - 0, - 0, - state.selected.image.width, - state.selected.image.height, - state.selected.x, - state.selected.y, - state.selected.w, - state.selected.h + eraseSelectedBox = state.selected.drawBox( + uiCtx, + {x, y}, + viewport.c2v ); - ovCtx.restore(); + } + }; - state.originalDisplayLayer.clear(); - state.originalDisplayLayer.ctx.save(); - state.originalDisplayLayer.ctx.drawImage( - state.selected.image, - 0, - 0, - state.selected.image.width, - state.selected.image.height, - state.selected.x, - state.selected.y, - state.selected.w, - state.selected.h + // Mouse Move Handler + state.movecb = (evn) => { + state.lastMouseMove = evn; + + // Get cursor positions + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); + + // Erase Cursor + eraseSelectedBox(); + eraseSelectedImage(); + eraseSelection(); + eraseCursor(); + imageCollection.inputElement.style.cursor = "default"; + + // Draw Box and Selected Image + if (state.selected) { + eraseSelectedBox = state.selected.drawBox( + uiCtx, + {x, y}, + viewport.c2v ); - state.originalDisplayLayer.ctx.restore(); - // Draw selection box - uiCtx.strokeStyle = "#FFF"; - uiCtx.setLineDash([4, 2]); - uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); - uiCtx.setLineDash([]); - - // Draw Scaling/Rotation Origin - uiCtx.beginPath(); - uiCtx.arc( - bbvp.x + bbvp.w / 2, - bbvp.y + bbvp.h / 2, - 5, - 0, - 2 * Math.PI - ); - uiCtx.stroke(); - - // Draw Scaling Handles - let cursorInHandle = false; - state.selected.handles().forEach((handle) => { - const bbvph = { - ...viewport.canvasToView(handle.x, handle.y), - w: 10, - h: 10, - }; - - bbvph.x -= 5; - bbvph.y -= 5; - - const inhandle = - evn.evn.clientX > bbvph.x && - evn.evn.clientX < bbvph.x + bbvph.w && - evn.evn.clientY > bbvph.y && - evn.evn.clientY < bbvph.y + bbvph.h; - - if (inhandle) { - cursorInHandle = true; - uiCtx.strokeRect( - bbvph.x - 1, - bbvph.y - 1, - bbvph.w + 2, - bbvph.h + 2 - ); - } else { - uiCtx.strokeRect(bbvph.x, bbvph.y, bbvph.w, bbvph.h); - } - }); - - // Change cursor - if (cursorInHandle || state.selected.contains(evn.x, evn.y)) + if ( + state.selected.hoveringBox(x, y) || + state.selected.hoveringHandle(x, y, viewport.zoom).onHandle || + state.selected.hoveringRotateHandle(x, y, viewport.zoom) + ) { imageCollection.inputElement.style.cursor = "pointer"; + } + + eraseSelectedImage = state.selected.drawImage( + state.originalDisplayLayer.ctx, + ovCtx, + {opacity: state.selectionPeekOpacity / 100} + ); } - // Draw current cursor location - state.erasePrevCursor = _tool._cursor_draw(x, y); + // Draw Selection + if (selection.exists) { + uiCtx.save(); + uiCtx.setLineDash([2, 2]); + uiCtx.lineWidth = 2; + uiCtx.strokeStyle = "#FFF"; - uiCtx.restore(); + const bbvp = selection.bb.transform(viewport.c2v); + uiCtx.beginPath(); + uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); + uiCtx.stroke(); + + eraseSelection = () => + uiCtx.clearRect( + bbvp.x - 10, + bbvp.y - 10, + bbvp.w + 20, + bbvp.h + 20 + ); + + uiCtx.restore(); + } + + // Draw cursor + eraseCursor = _tool._cursor_draw(sx, sy); }; // Handles left mouse clicks state.clickcb = (evn) => { if ( - !state.original || - (state.originalLayer === uil.layer && - state.original.x === state.selected.x && - state.original.y === state.selected.y && - state.original.w === state.selected.w && - state.original.h === state.selected.h) + state.selected && + !( + state.selected.rotation === 0 && + state.selected.scale.x === 1 && + state.selected.scale.y === 1 && + state.selected.position.x === state.original.sx && + state.selected.position.y === state.original.sy && + state.original.layer === uil.layer + ) ) { - state.reset(); - return; - } - - // If something is selected, commit changes to the canvas - if (state.selected) { - state.originalLayer.ctx.drawImage( - state.selected.image, + // Put original image back + state.original.layer.ctx.drawImage( + state.selected.canvas, state.original.x, state.original.y ); - commands.runCommand("eraseImage", "Image Transform Erase", { - ...state.original, - ctx: state.originalLayer.ctx, + + // Erase Original Selection Area + commands.runCommand("eraseImage", "Transform Tool Erase", { + ctx: state.original.layer.ctx, + x: state.original.x, + y: state.original.y, + w: state.selected.canvas.width, + h: state.selected.canvas.height, }); - commands.runCommand("drawImage", "Image Transform Draw", { - image: state.selected.image, - x: Math.round(state.selected.x), - y: Math.round(state.selected.y), - w: Math.round(state.selected.w), - h: Math.round(state.selected.h), + + // Draw Image + const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, { + border: 10, }); + commands.runCommand("drawImage", "Transform Tool Apply", { + image: canvas, + ...bb, + }); + state.reset(true); + } else { + state.reset(); + } + }; + + // Handles left mouse drag start events + state.dragstartcb = (evn) => { + const { + x: ix, + y: iy, + sx: six, + sy: siy, + } = _tool._process_cursor({x: evn.ix, y: evn.iy}, state.snapToGrid); + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); + + if (state.selected) { + const hoveringBox = state.selected.hoveringBox(ix, iy); + const hoveringHandle = state.selected.hoveringHandle( + ix, + iy, + viewport.zoom + ); + const hoveringRotateHandle = state.selected.hoveringRotateHandle( + ix, + iy, + viewport.zoom + ); + + if (hoveringBox) { + // Start dragging + moving = { + selected: state.selected.position, + offset: { + x: six - state.selected.position.x, + y: siy - state.selected.position.y, + }, + }; + return; + } else if (hoveringHandle.onHandle) { + // Start scaling + let handle = {x: 0, y: 0}; + + const lbb = new BoundingBox({ + x: -state.selected.canvas.width / 2, + y: -state.selected.canvas.height / 2, + w: state.selected.canvas.width, + h: state.selected.canvas.height, + }); + + if (hoveringHandle.ontl) { + handle = lbb.tl; + } else if (hoveringHandle.ontr) { + handle = lbb.tr; + } else if (hoveringHandle.onbl) { + handle = lbb.bl; + } else { + handle = lbb.br; + } + + scaling = { + handle, + }; + return; + } else if (hoveringRotateHandle) { + rotating = true; + return; + } + } + selection.dragstartcb(evn); + }; + + const transform = (evn, x, y, sx, sy) => { + if (moving) { + state.selected.position = { + x: sx - moving.offset.x, + y: sy - moving.offset.y, + }; + } + + if (scaling) { + /** @type {DOMMatrix} */ + const m = state.selected.rtmatrix.invertSelf(); + const lscursor = m.transformPoint({x: sx, y: sy}); + + const xs = lscursor.x / scaling.handle.x; + const xy = lscursor.y / scaling.handle.y; + + if (!state.keepAspectRatio) state.selected.scale = {x: xs, y: xy}; + else { + const scale = Math.max(xs, xy); + state.selected.scale = {x: scale, y: scale}; + } + } + + if (rotating) { + const center = state.selected.matrix.transformPoint({x: 0, y: 0}); + let angle = Math.atan2(x - center.x, center.y - y); + + if (evn.evn.shiftKey) + angle = + config.rotationSnappingAngles.find( + (v) => Math.abs(v - angle) < config.rotationSnappingDistance + ) ?? angle; + + state.selected.rotation = angle; } }; // Handles left mouse drag events - state.dragstartcb = (evn) => { - let ix = evn.ix; - let iy = evn.iy; - if (state.snapToGrid) { - ix += snap(evn.ix, 0, 64); - iy += snap(evn.iy, 0, 64); - } + state.dragcb = (evn) => { + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); - // If is selected, check if drag is in handles/body and act accordingly - if (state.selected) { - const handles = state.selected.handles(); + if (state.selected) transform(evn, x, y, sx, sy); - const activeHandle = handles.find((v) => { - const vpc = viewport.canvasToView(v.x, v.y); - const tlc = viewport.viewToCanvas(vpc.x - 5, vpc.y - 5); - const brc = viewport.viewToCanvas(vpc.x + 5, vpc.y + 5); - const bb = { - x: tlc.x, - y: tlc.y, - w: brc.x - tlc.x, - h: brc.y - tlc.y, - }; - - return ( - evn.ix > bb.x && - evn.ix < bb.x + bb.w && - evn.iy > bb.y && - evn.iy < bb.y + bb.h - ); - }); - if (activeHandle) { - state.scaling = activeHandle; - return; - } else if (state.selected.contains(ix, iy)) { - state.moving = { - offset: {x: ix - state.selected.x, y: iy - state.selected.y}, - }; - return; - } - } - // If it is not, just create new selection - state.reset(); - state.dragging = {ix, iy}; + if (selection.exists) selection.dragcb(evn); }; // Handles left mouse drag end events state.dragendcb = (evn) => { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, 0, 64); - y += snap(evn.y, 0, 64); + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); + + if (selection.exists) { + selection.dragendcb(evn); + + const bb = selection.bb; + + state.reset(); + + if (selection.exists && bb.w !== 0 && bb.h !== 0) { + const canvas = document.createElement("canvas"); + canvas.width = bb.w; + canvas.height = bb.h; + canvas + .getContext("2d") + .drawImage( + uil.canvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); + + uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h); + + state.original = { + ...bb, + sx: selection.bb.center.x, + sy: selection.bb.center.y, + layer: uil.layer, + }; + state.selected = new _tool.MarqueeSelection(canvas, bb.center); + } + + selection.deselect(); } - // If we are scaling, stop scaling and do some handler magic - if (state.scaling) { - state.selected.updateOriginal(); - state.scaling = null; - // If we are moving the selection, just... stop - } else if (state.moving) { - state.moving = null; - /** - * If we are dragging, create a cutout selection area and save to an auxiliar image - * We will be rendering the image to the overlay, so it will not be noticeable - */ - } else if (state.dragging) { - state.original = selectionBB( - state.dragging.ix, - state.dragging.iy, - x, - y - ); - state.selected = selectionBB( - state.dragging.ix, - state.dragging.iy, - x, - y - ); - state.originalLayer = uil.layer; - state.originalDisplayLayer = imageCollection.registerLayer(null, { - after: uil.layer, - category: "select-display", - }); + if (state.selected) transform(evn, x, y, sx, sy); - // Cut out selected portion of the image for manipulation - const cvs = document.createElement("canvas"); - cvs.width = state.selected.w; - cvs.height = state.selected.h; - const ctx = cvs.getContext("2d"); + moving = null; + scaling = null; + rotating = false; - ctx.drawImage( - uil.canvas, - state.selected.x, - state.selected.y, - state.selected.w, - state.selected.h, - 0, - 0, - state.selected.w, - state.selected.h - ); - - uil.ctx.clearRect( - state.selected.x, - state.selected.y, - state.selected.w, - state.selected.h - ); - state.selected.image = cvs; - state.original.image = cvs; - - if (state.selected.w === 0 || state.selected.h === 0) - state.selected = null; - - state.dragging = null; - } state.redraw(); }; @@ -550,11 +471,11 @@ const selectTransformTool = () => ctx.clearRect(0, 0, state.selected.w, state.selected.h); ctx.drawImage( - state.selected.image, + state.selected.canvas, 0, 0, - state.selected.image.width, - state.selected.image.height, + state.selected.canvas.width, + state.selected.canvas.height, 0, 0, state.selected.w, @@ -684,7 +605,7 @@ const selectTransformTool = () => saveSelectionButton.onclick = () => { downloadCanvas({ cropToContent: false, - canvas: state.selected.image, + canvas: state.selected.canvas, }); }; @@ -695,7 +616,7 @@ const selectTransformTool = () => createResourceButton.title = "Saves Selection as a Resource"; createResourceButton.onclick = () => { const image = document.createElement("img"); - image.src = state.selected.image.toDataURL(); + image.src = state.selected.canvas.toDataURL(); image.onload = () => { tools.stamp.state.addResource("Selection Resource", image); tools.stamp.enable(); diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index fc1cb89..2ac60bd 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -1,3 +1,41 @@ +/** + * Generic wheel handler + */ +let _stamp_wheel_accum = 0; + +const _stamp_onwheel = (evn, state) => { + if (evn.mode !== WheelEvent.DOM_DELTA_PIXEL) { + // We don't really handle non-pixel scrolling + return; + } + + let delta = evn.delta; + if (evn.evn.shiftKey) delta *= 0.01; + + // A simple but (I hope) effective fix for mouse wheel behavior + _stamp_wheel_accum += delta; + + if ( + !evn.evn.shiftKey && + Math.abs(_stamp_wheel_accum) > config.wheelTickSize + ) { + // Snap to next or previous position + const v = + state.scale - 0.1 * (_stamp_wheel_accum / Math.abs(_stamp_wheel_accum)); + + state.setScale(v + snap(v, 0, 0.1)); + state.redraw(evn); + + _stamp_wheel_accum = 0; // Zero accumulation + } else if (evn.evn.shiftKey && Math.abs(_stamp_wheel_accum) >= 1) { + const v = state.scale - _stamp_wheel_accum * 0.01; + state.setScale(v); + state.redraw(evn); + + _stamp_wheel_accum = 0; // Zero accumulation + } +}; + const stampTool = () => toolbar.registerTool( "./res/icons/file-up.svg", @@ -14,6 +52,12 @@ const stampTool = () => mouse.listen.world.btn.left.onclick.on(state.drawcb); mouse.listen.world.btn.right.onclick.on(state.cancelcb); + 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.onwheel.on(state.onwheelcb); + // For calls from other tools to paste image if (opt && opt.image) { state.addResource( @@ -41,6 +85,12 @@ const stampTool = () => mouse.listen.world.btn.left.onclick.clear(state.drawcb); mouse.listen.world.btn.right.onclick.clear(state.cancelcb); + 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.onwheel.clear(state.onwheelcb); + ovLayer.clear(); }, { @@ -54,7 +104,15 @@ const stampTool = () => state.lastMouseMove = {x: 0, y: 0}; state.block_res_change = true; + // Current Rotation + let rotation = 0; + let rotating = null; + // Current Scale + state.scale = 1; + state.selectResource = (resource, nolock = true, deselect = true) => { + rotation = 0; + state.setScale(1); if (nolock && state.ctxmenu.uploadButton.disabled) return; console.debug( @@ -290,32 +348,65 @@ const stampTool = () => syncResources(); }; - state.movecb = (evn) => { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, 0, 64); - y += snap(evn.y, 0, 64); + state.onwheelcb = (evn) => { + _stamp_onwheel(evn, state); + }; + + state.dragstartcb = (evn) => { + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); + rotating = {x: sx, y: sy}; + }; + + state.dragcb = (evn) => { + if (rotating) { + rotation = Math.atan2(rotating.x - evn.x, evn.y - rotating.y); + + if (evn.evn.shiftKey) + rotation = + config.rotationSnappingAngles.find( + (v) => + Math.abs(v - rotation) < config.rotationSnappingDistance + ) ?? rotation; } + }; - const vpc = viewport.canvasToView(x, y); - uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); - state.erasePrevCursor && state.erasePrevCursor(); + state.dragendcb = (evn) => { + rotating = null; + }; - uiCtx.save(); + let erasePrevCursor = () => null; + + state.movecb = (evn) => { + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); + + // Erase Previous Cursors + erasePrevCursor(); state.lastMouseMove = evn; ovLayer.clear(); + let px = sx; + let py = sy; + + if (rotating) { + px = rotating.x; + py = rotating.y; + } + // Draw selected image if (state.selected) { - ovCtx.drawImage(state.selected.image, x, y); + ovCtx.save(); + ovCtx.translate(px, py); + ovCtx.scale(state.scale, state.scale); + ovCtx.rotate(rotation); + + ovCtx.drawImage(state.selected.image, 0, 0); + ovCtx.restore(); } // Draw current cursor location - state.erasePrevCursor = _tool._cursor_draw(x, y); - uiCtx.restore(); + erasePrevCursor = _tool._cursor_draw(px, py); }; state.redraw = () => { @@ -323,20 +414,16 @@ const stampTool = () => }; state.drawcb = (evn) => { - let x = evn.x; - let y = evn.y; - if (state.snapToGrid) { - x += snap(evn.x, 0, 64); - y += snap(evn.y, 0, 64); - } + const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); const resource = state.selected; if (resource) { + const {canvas, bb} = cropCanvas(ovCanvas, {border: 10}); commands.runCommand("drawImage", "Image Stamp", { - image: resource.image, - x, - y, + image: canvas, + x: bb.x, + y: bb.y, }); if (resource.temporary) { @@ -380,6 +467,16 @@ const stampTool = () => ); state.ctxmenu.snapToGridLabel = array; + // Scale Slider + const scaleSlider = _toolbar_input.slider(state, "scale", "Scale", { + min: 0.01, + max: 10, + step: 0.1, + textStep: 0.001, + }); + state.ctxmenu.scaleSlider = scaleSlider.slider; + state.setScale = scaleSlider.setValue; + // Create resource list const uploadButtonId = `upload-btn-${guid()}`; @@ -528,6 +625,7 @@ const stampTool = () => }, populateContextMenu: (menu, state) => { menu.appendChild(state.ctxmenu.snapToGridLabel); + menu.appendChild(state.ctxmenu.scaleSlider); menu.appendChild(state.ctxmenu.resourceManager); }, shortcut: "U", diff --git a/pages/configuration.html b/pages/configuration.html index ac40172..a182188 100644 --- a/pages/configuration.html +++ b/pages/configuration.html @@ -5,15 +5,15 @@ openOutpaint 🐠 - + - + - + - + diff --git a/pages/embed.test.html b/pages/embed.test.html index 729eacd..b4ac0ab 100644 --- a/pages/embed.test.html +++ b/pages/embed.test.html @@ -8,8 +8,8 @@