确定选择框是否在旋转的矩形上

Determine if a Selection Marquee is over a Rotated Rectangle

我有一个矩形 class 用于绘制 HTML Canvas。它有一个旋转 属性 应用于它的 draw 方法。如果用户在 canvas 内拖动,则会绘制选择框。当矩形位于选择框 内时,如何使用数学 将矩形的 active 属性设置为 true?这是我在另一种语言和上下文中遇到的问题,所以我没有所有可用的 Canvas' 方法(例如 isPointInPath)。

我发现了一个关于查找 Mouse position within rotated rectangle in HTML5 Canvas 的 Whosebug post,我正在 Rectangle 方法 checkHit 中实现它。但是,它不考虑选择框。它只是看着鼠标 X 和 Y,它仍然处于关闭状态。浅蓝色点是矩形旋转的原点。如果有任何不清楚的地方,请告诉我。谢谢。

class Rectangle
{
  constructor(x, y, width, height, rotation) {
    this.x = x;
    this.y = y;
    this.height = height;
    this.width = width;
    this.xOffset = this.x + this.width/2;
    this.yOffset = this.y + ((this.y+this.height)/2);
    this.rotation = rotation;
    this.active = false;
  }
  
  checkHit()
  {
    // translate mouse point values to origin
    let originX = this.xOffset;
    let originY = this.yOffset;
    let dx = marquee[2] - originX;
    let dy = marquee[3] - originY;
    // distance between the point and the center of the rectangle
    let h1 = Math.sqrt(dx*dx + dy*dy);
    let currA = Math.atan2(dy,dx);
    // Angle of point rotated around origin of rectangle in opposition
    let newA = currA - this.rotation;
    // New position of mouse point when rotated
    let x2 = Math.cos(newA) * h1;
    let y2 = Math.sin(newA) * h1;
    // Check relative to center of rectangle
    if (x2 > -0.5 * this.width && x2 < 0.5 * this.width && y2 > -0.5 * this.height && y2 < 0.5 * this.height){
      this.active = true;
    } else {
      this.active = false;    
    }
    
  }
  
  draw()
  {
    ctx.save();
    ctx.translate(this.xOffset, this.yOffset);
    ctx.fillStyle = 'rgba(255,255,255,1)';
    ctx.beginPath();
    ctx.arc(0, 0, 3, 0, 2 * Math.PI, true);
    ctx.fill();
    ctx.rotate(this.rotation * Math.PI / 180);
    ctx.translate(-this.xOffset, -this.yOffset);
    if (this.active)
    {
      ctx.fillStyle = 'rgba(255,0,0,0.5)';
    } else {
      ctx.fillStyle = 'rgba(0,0,255,0.5)';      
    }
    ctx.beginPath();
    ctx.fillRect(this.x, this.y, this.width, this.y+this.height);
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
  }
}

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var raf;
var rect = new Rectangle(50,50,90,30,45);
var marquee = [-3,-3,-3,-3];
var BB=canvas.getBoundingClientRect();
var offsetX=BB.left;
var offsetY=BB.top;
var start_x,start_y;

let draw = () => {
  ctx.clearRect(0,0, canvas.width, canvas.height);
  //rect.rotation+=1;
  rect.draw();
  ctx.fillStyle = "rgba(200, 200, 255, 0.5)";
  ctx.fillRect(parseInt(marquee[0]),parseInt(marquee[1]),parseInt(marquee[2]),parseInt(marquee[3]))
  ctx.strokeStyle = "white"
  ctx.lineWidth = 1;
  ctx.rect(parseInt(marquee[0]),parseInt(marquee[1]),parseInt(marquee[2]),parseInt(marquee[3]))
  ctx.stroke()
  raf = window.requestAnimationFrame(draw);
}

let dragStart = (e) =>
{
  start_x = parseInt(e.clientX-offsetX);
  start_y = parseInt(e.clientY-offsetY);
  marquee = [start_x,start_y,0,0];
  canvas.addEventListener("mousemove", drag);
}

let drag = (e) =>
{
  let mouseX = parseInt(e.clientX-offsetX);
  let mouseY = parseInt(e.clientY-offsetY);
  marquee[2] = mouseX - start_x;
    marquee[3] = mouseY - start_y;
  rect.checkHit();
}

let dragEnd = (e) =>
{
  marquee = [-10,-10,-10,-10];
  canvas.removeEventListener("mousemove", drag);
}

canvas.addEventListener('mousedown', dragStart);
canvas.addEventListener('mouseup', dragEnd);

raf = window.requestAnimationFrame(draw);
body
{
  margin:0;  
}

#canvas
{
  width: 360px;
  height: 180px;
  border: 1px solid grey;
  background-color: grey;
}
<canvas id="canvas" width="360" height="180"></canvas>

凸多边形是否重叠

矩形是凸多边形。

Rectanglemarquee 各有 4 个点(角),定义了连接这些点的 4 条边(线段)。

此解决方案适用于所有具有 3 个或更多边的凸不规则多边形。

点和边必须是顺时针 CW 或 Count Clockwise CCW 顺序

测试点

如果一个多边形的任何点在另一个多边形内,则它们必须重叠。请参阅示例函数 isInside

要检查点是否在多边形内部,求叉积,点的边开始作为向量,边作为向量。

如果所有叉积 >= 0(在其左侧)则存在重叠(对于 CW 多边形)。如果多边形是逆时针,则如果所有叉积 <= 0(在其右侧),则存在重叠。

可以在另一个多边形内没有任何点的情况下重叠。

测试边缘

如果一个多边形的任何边与另一个多边形的任何边相交,则一定存在重叠。如果两条线段相交,函数 doLinesIntercept return 为真。

完成测试

如果两个多边形有重叠,函数 isPolyOver(poly1, poly2) 将 return true

多边形由一组 PointLines 的连接点定义。

多边形可以是不规则的,这意味着每条边的长度都可以> 0

不要传递边长 === 0 的多边形,否则将不起作用。

已添加

我添加了函数 Rectangle.toPoints 来变换矩形和 return 一组 4 个点(角)。

例子

示例是使用上述方法工作的代码的副本。

canvas.addEventListener('mousedown', dragStart);
canvas.addEventListener('mouseup', dragEnd);
requestAnimationFrame(draw);

const Point = (x = 0, y = 0) => ({x, y, set(x,y){ this.x = x; this.y = y }});
const Line = (p1, p2) => ({p1, p2});
const selector = { points: [Point(), Point(), Point(), Point()] }
selector.lines = [
    Line(selector.points[0], selector.points[1]),
    Line(selector.points[1], selector.points[2]),
    Line(selector.points[2], selector.points[3]),
    Line(selector.points[3], selector.points[0])
];
const rectangle = { points: [Point(), Point(), Point(), Point()] }
rectangle.lines = [
    Line(rectangle.points[0], rectangle.points[1]),
    Line(rectangle.points[1], rectangle.points[2]),
    Line(rectangle.points[2], rectangle.points[3]),
    Line(rectangle.points[3], rectangle.points[0])
];

function isInside(point, points) {
    var i = 0, p1 = points[points.length - 1];
    while (i < points.length) {
        const p2 = points[i++];
        if ((p2.x - p1.x) * (point.y - p1.y) - (p2.y - p1.y) * (point.x - p1.x) < 0) { return false }
        p1 = p2;
    }
    return true;
}
function doLinesIntercept(l1, l2) { 
    const v1x = l1.p2.x - l1.p1.x;
    const v1y = l1.p2.y - l1.p1.y;
    const v2x = l2.p2.x - l2.p1.x;
    const v2y = l2.p2.y - l2.p1.y;
    const c = v1x * v2y - v1y * v2x;
    if(c !== 0){
        const u = (v2x * (l1.p1.y - l2.p1.y) - v2y * (l1.p1.x - l2.p1.x)) / c;
        if(u >= 0 && u <= 1){
            const u = (v1x * (l1.p1.y - l2.p1.y) - v1y * (l1.p1.x - l2.p1.x)) / c;
            return  u >= 0 && u <= 1;
        }
    }
    return false;
}   
function isPolyOver(p1, p2) { // is poly p2 under any part of poly p1
    if (p2.points.some(p => isInside(p, p1.points))) { return true };
    if (p1.points.some(p => isInside(p, p2.points))) { return true };
    return p1.lines.some(l1 => p2.lines.some(l2 => doLinesIntercept(l1, l2)));
}
    
const ctx = canvas.getContext("2d");
var dragging = false;

const marquee = [0,0,0,0];
const rotate = 0.01;
var startX, startY, hasSize = false;
const BB = canvas.getBoundingClientRect();
const offsetX = BB.left;
const offsetY = BB.top;
class Rectangle {
    constructor(x, y, width, height, rotation) {
        this.x = x;
        this.y = y;
        this.height = height;
        this.width = width;
        this.rotation = rotation;
        this.active = false;
    }
    toPoints(points = [Point(), Point(), Point(), Point()]) {
        const xAx = Math.cos(this.rotation) / 2;
        const xAy = Math.sin(this.rotation) / 2;
        const x = this.x, y = this.y;
        const w = this.width, h = this.height;
        points[0].set(-w * xAx + h * xAy + x, -w * xAy - h * xAx + y);
        points[1].set( w * xAx + h * xAy + x,  w * xAy - h * xAx + y);
        points[2].set( w * xAx - h * xAy + x,  w * xAy + h * xAx + y);
        points[3].set(-w * xAx - h * xAy + x, -w * xAy + h * xAx + y);
    }
    draw() {
        ctx.setTransform(1, 0, 0, 1, this.x, this.y);
        ctx.fillStyle = 'rgba(255,255,255,1)';
        ctx.strokeStyle = this.active ? 'rgba(255,0,0,1)' : 'rgba(0,0,255,1)';
        ctx.lineWidth = this.active ? 3 : 1;
        
        ctx.beginPath();
        ctx.arc(0, 0, 3, 0, 2 * Math.PI, true);
        ctx.fill();
        ctx.rotate(this.rotation);
        
        ctx.beginPath();
        ctx.rect(-this.width / 2, - this.height / 2, this.width, this.height);
        ctx.stroke();
    }
}
function draw(){
    rect.rotation += rotate;
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    rect.draw();
    drawSelector();
    requestAnimationFrame(draw);
}
function drawSelector() {
    if (dragging && hasSize) {
        rect.toPoints(rectangle.points);
        rect.active = isPolyOver(selector, rectangle);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.fillStyle = "rgba(200, 200, 255, 0.5)";
        ctx.strokeStyle = "white";
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.rect(...marquee);
        ctx.fill();
        ctx.stroke();
    
    } else {
        rect.active = false;
    }

 
}
function dragStart(e) {
    startX = e.clientX - offsetX;
    startY = e.clientY - offsetY;
    drag(e);
    canvas.addEventListener("mousemove", drag);
    
}
function drag(e) {
    dragging = true;
    const x = e.clientX - offsetX;
    const y = e.clientY - offsetY;
    const left = Math.min(startX, x);
    const top = Math.min(startY, y);
    const w = Math.max(startX, x) - left;
    const h = Math.max(startY, y) - top;
    marquee[0] = left;
    marquee[1] = top;
    marquee[2] = w;
    marquee[3] = h;
    if (w > 0 || h > 0) {
        hasSize = true;
        selector.points[0].set(left,   top);
        selector.points[1].set(left + w, top);
        selector.points[2].set(left + w, top + h);
        selector.points[3].set(left  , top + h);
        
    } else {
        hasSize = false;
    }
}
function dragEnd(e) {
    dragging = false;
    rect.active = false;
    canvas.removeEventListener("mousemove", drag);
}

const rect = new Rectangle(canvas.width / 2, canvas.height / 2, 90, 90, Math.PI / 4);
body
{
  margin:0;  
}

#canvas
{
  width: 360px;
  height: 180px;
  border: 1px solid grey;
  background-color: grey;
}
<canvas id="canvas" width="360" height="180"></canvas>