如何计算旋转角度使宽度在透视模式下适合所需尺寸?

3
我正在尝试通过CSS的方式来绕Y轴进行图像透视旋转,使得最终可见宽度等于期望的像素数。例如,我可能想要将一个300px的图像旋转,以便在应用旋转和透视后,图像的宽度现在是240px(原始值的80%)。通过试错,我知道我可以设置transform: perspective(300) rotateY(-12.68),并将左上角点置于-240px(这是使用图像右侧作为起点)。我还没有完全弄清楚如何反向工程化,以便为任何给定的图像宽度、透视和期望宽度计算必要的旋转角度。举个例子,对于同样的300px图像,我现在希望旋转后它的宽度为150px - 需要哪些计算才能得到必要的角度?这里有一个游乐场,让你了解我所寻找的内容,我已经复制了透视和旋转变换所做的数学计算,以计算左侧点的最终位置,但我还没有能够弄清楚如何解决由矩阵数学和多个步骤组成的角度问题。:https://repl.it/@BenSlinger/PerspectiveWidthDemo

const calculateLeftTopPointAfterTransforms = (perspective, rotation, width) => {

  // convert degrees to radians
  const rRad = rotation * (Math.PI / 180);

  // place the camera
  const cameraMatrix = math.matrix([0, 0, -perspective]);

  // get the upper left point of the image based on middle right transform origin
  const leftMostPoint = math.matrix([-width, -width / 2, 0]);

  const rotateYMatrix = math.matrix([
    [Math.cos(-rRad), 0, -Math.sin(-rRad)],
    [0, 1, 0],
    [Math.sin(-rRad), 0, Math.cos(-rRad)],
  ]);

  // apply rotation to point
  const rotatedPoint = math.multiply(rotateYMatrix, leftMostPoint);

  const cameraProjection = math.subtract(rotatedPoint, cameraMatrix);

  const pointInHomogenizedCoords = math.multiply(math.matrix([
    [1, 0, 0 / perspective, 0],
    [0, 1, 0 / perspective, 0],
    [0, 0, 1, 0],
    [0, 0, 1 / perspective, 0],
  ]), cameraProjection.resize([4], 1));

  const finalPoint = [
    math.subset(pointInHomogenizedCoords, math.index(0))
    / math.subset(pointInHomogenizedCoords, math.index(3)),
    math.subset(pointInHomogenizedCoords, math.index(1))
    / math.subset(pointInHomogenizedCoords, math.index(3)),
  ];

  return finalPoint;
}
<div id="app"></div>


 <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.js"></script>
 
  <script type="text/babel" data-plugins="transform-class-properties" >
  // GOAL: Given the percentage defined in desiredWidth, calculate the rotation required for the transformed image to fill that space (shown by red background)

// eg: With desiredWidth 80 at perspective 300 and image size 300, rotation needs to be 12.68, putting the left point at 300 * .8 = 240.
// How do I calculate that rotation for any desired width, perspective and image size?


// factor out some styles
const inputStyles = { width: 50 };

const PerspDemo = () => {

  const [desiredWidth, setDesiredWidth] = React.useState(80);
  const [rotation, setRotation] = React.useState(25);
  const [perspective, setPerspective] = React.useState(300);
  const [imageSize, setImageSize] = React.useState(300);
  const [transformedPointPosition, setTPP] = React.useState([0, 0]);

  const boxStyles = { outline: '1px solid red', width: imageSize + 'px', height: imageSize + 'px', margin: '10px', position: 'relative' };

  React.useEffect(() => {
    setTPP(calculateLeftTopPointAfterTransforms(perspective, rotation, imageSize))
  }, [rotation, perspective]);


  return <div>
    <div>
      <label>Image size</label>
      <input
        style={inputStyles}
        type="number"
        onChange={(e) => setImageSize(e.target.value)}
        value={imageSize}
      />
    </div>
    <div>
      <label>Desired width after transforms (% of size)</label>
      <input
        style={inputStyles}
        type="number"
        onChange={(e) => setDesiredWidth(e.target.value)}
        value={desiredWidth}
      />
    </div>

    <div>
      <label>Rotation (deg)</label>
      <input
        style={inputStyles}
        type="number"
        onChange={(e) => setRotation(e.target.value)}
        value={rotation}
      />
    </div>

    <div>
      <label>Perspective</label>
      <input
        style={inputStyles}
        type="number"
        onChange={(e) => setPerspective(e.target.value)}
        value={perspective}
      />
    </div>



    <div>No transforms:</div>
    <div style={boxStyles}>
      <div>
        <img src={`https://picsum.photos/${imageSize}/${imageSize}`} />
      </div>
    </div>

    <div>With rotation and perspective:</div>
    <div style={boxStyles}>
      <div style={{ display: 'flex', position: 'absolute', height: '100%', width: '100%' }}>
        <div style={{ backgroundColor: 'white', flexBasis: 100 - desiredWidth + '%' }} />
        <div style={{ backgroundColor: 'red', flexGrow: 1 }} />

      </div>
      <div style={{
        transform: `perspective(${perspective}px) rotateY(-${rotation}deg)`,
        transformOrigin: '100% 50% 0'
      }}>
        <img src={`https://picsum.photos/${imageSize}/${imageSize}`} />
      </div>
    </div>
    <div>{transformedPointPosition.toString()}</div>
  </div>;
};

ReactDOM.render(<PerspDemo />, document.getElementById('app'));

  </script>
  
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/6.0.4/math.min.js"></script>

非常感谢任何帮助!


如果您了解数学规则,可以使用JS计算新宽度并设置它。请尝试查看此链接:https://dev59.com/b3PYa4cB1Zd3GeqPfBtr#46107713 ,也许您可以在数学专业社区提问。 - red
@red,抱歉,我的问题可能没有表达清楚。我知道如何解决宽度的问题,实际上我正在尝试解决角度的问题,以便最终的图像将是给定的宽度。我已经编辑了问题以澄清。(我尝试在math.stackexchange.com上提问,但被认为是不适当的。) - Ben Slinger
1个回答

6

我会考虑一种不需要矩阵计算的方法来得出以下公式:1

R = (p * cos(angle) * D)/(p - (sin(angle) * D))

其中,p 表示透视,angle 表示旋转角度,D 表示元素宽度,R 表示我们要搜索的新宽度。

如果我们有一个角度为 -45deg,透视为 100px,初始宽度为 200px,那么新宽度将为:58.58px

.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
  background:
    linear-gradient(red,red) right/58.58px 100% no-repeat;
  position:relative;
}

img {
 transform-origin:right;
}
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform:perspective(100px) rotateY(-45deg)">
</div>

如果我们有一个角度为-30deg,透视等于200px和初始宽度200px,那么新的宽度将是115.46px

.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
  background:
    linear-gradient(red,red) right/115.46px 100% no-repeat;
  position:relative;
}

img {
 transform-origin:right;
}
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform:perspective(200px) rotateY(-30deg)">
</div>

1 为了更好地理解公式,让我们考虑下面的图示:

enter image description here

想象一下,我们从上面看到了所有的东西。红线是我们旋转的元素。大黑点是我们的视角,距离场景有一个等于p的距离(这是我们的视角)。由于transform-origin在右侧,将这个点放在右侧是合理的。否则,它应该在中心。

现在,我们看到的宽度由RW设计,是我们在没有透视的情况下看到的宽度。很明显,透视越大,我们看到的没有透视的情况下几乎相同。

.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
}

img {
 transform-origin:right;
}
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform: rotateY(-30deg)">
</div>
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform:perspective(9999px) rotateY(-30deg)">
</div>

当我们从一个小的角度看时,我们看到了一个小的宽度。

.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
}

img {
 transform-origin:right;
}
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform: rotateY(-30deg)">
</div>
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform:perspective(15px) rotateY(-30deg)">
</div>

如果我们考虑图中由O表示的角度,我们可以写出以下公式:

tan(O) = R/p

并且

tan(O) = W/(L + p)

因此,我们将有 R = p*W /(L + p),其中 W = cos(-angle)*D = cos(angle)*DL = sin(-angle)*D = -sin(angle)*D,这将给我们:

R = (p * cos(angle) * D)/(p - (sin(angle) * D))

为了找到角度,我们可以将公式转换为:
R*p - R*D*sin(angle) = p*D*cos(angle)
R*p = D*(p*cos(angle) + R*sin(angle))

然后,就像这里1所描述的那样,我们可以得到以下方程:

angle = sin-1((R*p)/(D*sqrt(p²+R²))) - tan-1(p/R)

如果您想要透视等于 190px,并且 R 等于 150px 以及 D 等于 200px,则需要旋转角度为 -15.635deg

.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
  background:
    linear-gradient(red,red) right/150px 100% no-repeat;
  position:relative;
}

img {
 transform-origin:right;
}
<div class="box">
  <img src="https://picsum.photos/id/1/200/200" style="transform:perspective(190px) rotateY(-15.635deg)">
</div>

1 感谢帮助我找到正确公式的https://math.stackexchange.com社区


@BenSlinger 我进行了第一次更新,加入了一个公式来找到角度,但只适用于特定情况。我猜想处理所有情况可能会很困难,因为在某些情况下,由于未定义,无法找到角度。 - Temani Afif
@BenSlinger 如果R与视角不同,那么很难使其正常工作,因为在某些情况下公式未定义。这非常复杂。我可以为其他特定情况提供更多的公式,但我需要定义R和p之间的关系。在这里,我考虑p=R,我也可以对“p=sqrt(3)*R”做同样的事情,但通用公式并不容易。顺便说一句,我已经找到了公式,这是困难的部分。现在,您需要找到所需的数学技巧,以根据其他属性表达角度。您将比矩阵计算更有好运。 - Temani Afif
@BenSlinger 在最终公式中,你有 sin(angle + 45deg)*(D*sqrt(2)) = R - Temani Afif
不幸的是,将视角和图像大小分离是必需的 - 我不需要单个公式,执行多个步骤也可以,我认为可能有一种方法重新排列步骤,包括矩阵变换以得出角度。 - Ben Slinger
@BenSlinger,终于有你的公式了 ;) 请查看更新。 - Temani Afif
显示剩余5条评论

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