Ben Slinger
Ben Slinger

Reputation: 267

How to calculate angle of rotation to make width fit desired size in perspective mode?

I am trying to come up with a way to rotate an image in perspective around the Y axis via CSS so that the final visible width equals a desired number of pixels.

For example, I might want to rotate a 300px image so that, after rotation and perspective is applied, the width of the image is now 240px (80% of original). By trial and error I know I can set transform: perspective(300) rotateY(-12.68) and it puts the top left point at -240px (this is using the right side of the image as the origin)

I can't quite figure out how to reverse engineer this so that for any given image width, perspective and desired width I can calculate the necessary rotation.

Eg. For the same 300px image, I now want it to be a width of 150px after rotation - what is the calculation required to get the necessary angle?

Here's a playground to give you an idea of what I'm looking for, I've replicated the math done by the perspective and rotation transforms to calculate the final position of the left-most point, but I haven't been able to figure out how to solve for the angle given the matrix math and multiple steps involved.

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>

Any help is much appreciated!

Upvotes: 3

Views: 1751

Answers (1)

Temani Afif
Temani Afif

Reputation: 272589

I would consider a different way to find the formula without matrix calculation1 to obtain the following:

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

Where p is the perspective and angle is the angle rotation and D is the element width and R is the new width we are searching for.

If we have an angle of -45deg and a perspective equal to 100px and an initial width 200px then the new width will be: 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>

If we have an angle of -30deg and a perspective equal to 200px and an initial width 200px then the new width will be 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 To better understand the formula let's consider the following figure:

enter image description here

Imagine that we are looking at everything from the top. The red line is our rotated element. The big black dot is our point of view with a distance equal to p from the scene (this is our perspective). Since the transform-origin is the right, it's logical to have this point at the right. Otherwise, it should at the center.

Now, what we see is the width designed by R and W is the width we see without perspective. It's clear that with a big perspective we see almost the same without perspective

.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>

and with a small perspective we see a small width

.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>

If we consider the angle noted by O in the figure we can write the following formula:

tan(O) = R/p

and

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

So we will have R = p*W /(L + p) with W = cos(-angle)*D = cos(angle)*D and L = sin(-angle)*D = -sin(angle)*D which will give us:

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

To find the angle we can transform our formula to be:

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

Then like described here1 we can obtain the following equation:

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

If you want a perspective equal to 190px and R equal to 150px and D equal to 200px you need a rotation equal to -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 Thanks to the https://math.stackexchange.com community that helped me identify the right formula

Upvotes: 6

Related Questions