openOutpaint/js/ui/tool/generic.js
Victor Seiji Hariki d764221d63 support select tool rotation
Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
2023-01-12 21:49:06 -03:00

657 lines
16 KiB
JavaScript

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