/** * 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] 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: global.hasActiveInput ? "#BBF" : "#FFF", }); const bbvp = bb.transform(viewport.c2v); 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] 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: global.hasActiveInput ? "#BBF5" : "#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 * * @returns {BoundingBox} */ 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; }, /** * 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.matrix.inverse().transformPoint({x, y}); const localrh = { x: 0, 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); }; } }, };