如何将一个矩形夹在外部旋转矩形的边界内?

18

我正在制作一个WebGL(带有2D画布回退的照片编辑器)。

我决定将旋转直接纳入裁剪工具中,方式类似于iOS 8照片裁剪器。也就是说,随着您旋转照片,照片的大小和位置会动态更改,以便裁剪区域始终包含在照片本身内。

但是,我遇到了一些数学问题。

我有两个矩形,照片和裁剪区域。

两者都定义为:

var rect = {
     x : x,
     y : y,
     w : width,
     h : height
}

照片本身定义的矩形也具有以弧度为单位的旋转属性,这当然描述了照片的角度(以弧度为单位)。

需要注意的是,照片矩形的xy坐标(而不是裁剪矩形)实际上定义了照片的中心,而不是左上角点。虽然这可能看起来很奇怪,但它使我们在其他地方的计算更容易,并且不应影响到这个问题。为了完整起见,我提一下。

照片的变换原点始终设置为裁剪区域的中心

当照片的角度为0且裁剪区域与照片的大小相同时,矩形对齐如下所示:

照片和裁剪区域对齐的图片

当我旋转照片时,会应用缩放因子以确保裁剪区域保持在照片的边界内:

缩放后的照片和裁剪区域对齐的图片

这是通过计算裁剪区域的边界框并从中计算出所需的缩放因子来实现的,然后应用于photo矩形:

TC.UI.PhotoCropper.prototype._GetBoundingBox = function(w,h,rads){
    var c = Math.abs(Math.cos(rads));
    var s = Math.abs(Math.sin(rads));
    return({  w: h * s + w * c,  h: h * c + w * s });
}

var bbox = this._GetBoundingBox(crop.w, crop.h, photo.rotation);
var scale = Math.max(1, Math.max(bbox.w/photo.w, bbox.h/photo.h));

这目前能够完美地实现设计意图,无论裁剪区域(以及旋转的基准点)在哪里,照片矩形的宽度都会正确缩放。

由于这是一个照片裁剪工具,当然可以修改裁剪区域的位置。请注意,当我们修改裁剪区域位置时,我们正在移动photo矩形本身(裁剪区域始终保持居中,因为我们认为这在触摸设备上更自然,在使用鼠标时仍然可接受...这在iOS和Windows 8中都是一样的)。

因此,我们目前将xy坐标限制在裁剪区域的边缘,如下所示:

var maxX = crop.x + (crop.w/2) - (photo.w/2);
var maxY = crop.y + (crop.h/2) - (photo.w/2);
var minX = crop.x - (crop.w/2) + (photo.w/2);
var minY = crop.y - (crop.h/2) + (photo.h/2);

photo.x = Math.min(minX, Math.max(maxX, photo.x));
photo.y = Math.min(minY, Math.max(maxY, photo.y));

这就是我遇到麻烦的地方。

当裁剪区域没有居中在照片内时,我们会发现裁剪区域移动到了照片边界之外,就像这样:

enter image description here

记住,旋转的原点始终位于裁剪区域的正中心。

从上面的照片中,您可以看到所需的宽度已正确计算,照片被正确缩放以包含裁剪区域。

然而,由于我们的夹紧函数只检查裁剪区域的边缘,所以我们最终得到的裁剪区域超出了照片的边界。

我们希望修改我们的夹紧函数,使其考虑矩形photo的旋转。因此,应正确夹紧(在上面的截图中)xy坐标,以确保最终结果如下:

enter image description here

不幸的是,我不知道从哪里开始数学计算,真的需要一些帮助。

我们目前在旋转和移动photo矩形时使用相同的夹紧函数,并希望继续这样做,如果可能的话。


1
良好的问题陈述,加1。 - Bergi
@Bergi 感谢,希望有人能帮忙! :) - gordyr
我太懒了,不想自己编写和测试代码,但是以下是一些提示,通常可以用来完成这个任务:1. 如果在裁剪/缩放之前使用缩放,则缩放应该在裁剪方程中。2. 尝试将 add/sub center_offset*cos(angle)center_offset*sin(angle) 或它们的一半添加到裁剪方程中以匹配所需的输出(center_offset 是旋转中心和裁剪中心之间的差异)。 - Spektre
@spektre 是的,缩放在 clamp 函数中。photo.w 和 photo.h 实际上是缩放后的值(可访问缩放因子),无论如何感谢你,我会按照你的建议尝试一下,看看能得到什么结果 :) - gordyr
3个回答

4
正如DARK_DUCK所说,有方法(非平凡的方法)可以编写一个clamp函数来解决您的问题。然而,当您说“clamp”时,我自动假设您的解决方案必须限制用户达到一些无效状态,这在我看来不是一个好的解决方案。也许用户会旋转图像以通过一些无效状态来达到有效状态。
我提出另一种解决方案:
  1. 将每次更改(旋转、平移、缩放)的最后一个有效状态记录下来
  2. 当用户完成更改时,如果当前状态有效,则不做任何操作,否则恢复上一个有效状态。
状态将包括图像起点(或位置)、旋转和缩放。
您可以通过动画从无效状态过渡到最后一个有效状态。 当处于无效状态时,您可以通过使裁剪区域的边框变为红色来向用户报告:example 有效状态是指裁剪区域的所有4个角都位于图像矩形内。 以下功能将帮助您测试这种条件:
//rotate a point around a origin
function RotatePoint(point, angle, origin)
{
    var a = angle * Math.PI / 180.0;
    var sina = Math.sin(a);
    var cosa = Math.cos(a);

    return
    {
        x : origin.x + cosa * (point.x - origin.x) - sina * (point.y - origin.y),
        y : origin.y + sina * (point.x - origin.x) + cosa * (point.y - origin.y)
    }

}

//position of point relative to line defined by a and b
function PointPos(a, b, point)
{
    return Math.sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
}

您需要:
  1. 计算应用旋转和平移后的图像矩形的角落位置
  2. 使用上面提供的第二个函数检查裁剪矩形角落是否在图像矩形内。如果一个点x在矩形内,则:

    PointPos(a,b,x) == -1 && PointPos(b,c,x) == -1 && PointPos(c,d,x) == -1 && PointPos(d,a,x) == -1;

编辑: 这里有一个例子:JSFiddle

这两个矩形的起点不同。外部(pictureArea)矩形的旋转原点是内部(cropArea)矩形的起点。

祝你好运!


谢谢B0Andrew... 这是一个非常有趣的解决方案。在接受您的建议作为正确答案之前,我将花一些时间来尝试它。请放心,如果最终采用了您的想法,我当然会授予您答案。再次感谢 :) - gordyr
另外,如果我能够计算所需的比例因子来放大图像,以便当前状态始终有效,那么这将更好。您的答案可能已经无意中为我提供了90%的解决方案。 - gordyr

4

问题出在哪里?

你需要在旋转的坐标系中夹紧照片坐标,而不是在原始坐标系中夹紧。在旋转坐标系中使用裁剪区域角落的坐标(计算minXmaxXminYmaxY时)没有意义。

夹紧照片坐标

您需要在旋转的坐标系中构建一个裁剪区域的轴对齐边界框,以便进行夹紧。就像这样:

旋转坐标系中的边界框

由于我们围绕裁剪区域的中心旋转,因此中心坐标保持不变。但是宽度和高度将增加。

请注意,您已经计算出这些数量以适当地缩放照片。因此,简而言之,您可以用计算边界框(bbox.wbbox.h)时获得的宽度crop.w和高度crop.h替换在缩放时使用的宽度和高度:

var maxX = crop.x + (bbox.w/2) - (photo.w/2);
var maxY = crop.y + (bbox.h/2) - (photo.w/2);
var minX = crop.x - (bbox.w/2) + (photo.w/2);
var minY = crop.y - (bbox.h/2) + (photo.h/2);

center = {
    x: Math.min(minX, Math.max(maxX, photo.x)),
    y: Math.min(minY, Math.max(maxY, photo.y))
};

请记住,最后两行获取的坐标仅适用于旋转坐标系。如果您需要它们在原始坐标系中,请将它们旋转回来:

var sin = Math.sin(-rotation);
var cos = Math.cos(-rotation);

photo.x = crop.x + cos*(center.x - crop.x) - sin*(center.y - crop.y);
photo.y = crop.y + sin*(center.x - crop.x) + cos*(center.y - crop.y);

1
实际上,您无法“夹紧”x和y值,因为x和y是相互依存的。我的意思是,您可以通过更改图像的x值或y值或x和y值的无限线性组合来解决问题(图像越界)。
因此,夹紧函数将返回一组无限正确的值(如果没有解决方案则不返回任何值)。如果您想要解决问题,您可能应该向夹紧函数的规范添加更多条件。例如,您可以说夹紧函数必须最小化max(displacementX, displacementY)。如果您添加了此条件,则夹紧函数将返回一个单一的解决方案或不返回任何解决方案。

谢谢DARK_DUCK...我想我明白你的意思了。问题是我没有足够的数学理解能力来编写这样的“clamp”函数。 - gordyr
我明白了,但如果我们给您的函数是最小化max(displacementX,displacementY)的函数,您是否可以接受?还是您想要另一个函数?我只是想说,对于您的问题,没有答案,只有无限多的解决方案。 - DARK_DUCK
如果我理解你的意思是正确的,那么是的,你建议的函数是合适的...并且非常感激 :) - gordyr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接