3

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!

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
Ben Slinger
  • 267
  • 4
  • 14
  • If you know the math rules you can calc the new width and set it with js. Try to look at this: https://stackoverflow.com/a/46107713/11368483 maybe you can ask in mathematics specific community – red Aug 09 '19 at 14:30
  • @red, sorry, my original question must not have been clear enough. I know how to solve for the width, I'm actually trying to solve for the angle so that the resulting image will be a given width. I've edited the question to clarify. (I tried asking in math.stackexchange.com and it was deemed off-topic) – Ben Slinger Aug 09 '19 at 23:48

1 Answers1

6

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

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • Hi Temani, thanks for the detailed response, but I think you've misunderstood the question. I'm already able to see what the width will be given an angle and perspective, but I'm trying to rearrange that equation so that, given a desired width (R in your example) I can calculate the angle required. So solving for the angle, as opposed to the width. Sorry if that wasn't clear from the original question, I'll edit it to clarify. – Ben Slinger Aug 09 '19 at 23:38
  • @BenSlinger I know, that's what is missing in my answer as noted at the end. I was able to identify the formula but now need to change it to express angle with the other parametres But we have at least the formula that we can start with – Temani Afif Aug 09 '19 at 23:40
  • Ah, gotcha, I didn't realize what you meant by that, sorry! I appreciate the help! – Ben Slinger Aug 10 '19 at 01:23
  • @BenSlinger I made a first update with a formula to find the angle but in a particular case. I guess it would be difficult to handle all the cases because in some cases it's impossible to find the angle as it's not defined – Temani Afif Aug 10 '19 at 11:41
  • thanks so much for your work - I'm not sure how to get this working when perpective doesn't equal image size though, which is generally not going to be the case. I'm also not quite sure how to determine the value of R in your final equation, is R not derived from the angle earlier up? – Ben Slinger Aug 11 '19 at 05:16
  • @BenSlinger it's not easy to get it working if R is different from perspective because the formula isn't defined in some case. well it's very complicated. I can get more formula for other particular cases but I need to define a relation between R and p. Here I concisder `p=R`, I can also do the same for ``p=sqrt(3)*R` but a generic formula is not easy. By the way, I have find the formula which is the difficult part. Now you need to find the needed math tricks to express the angle based on the other properties. You will have better luck than the matrix calculation – Temani Afif Aug 11 '19 at 22:00
  • @BenSlinger in the final formula you have `sin(angle + 45deg)*(D*sqrt(2)) = R` – Temani Afif Aug 11 '19 at 22:01
  • Unfortunately separating perspective and image size is a requirement - I don't need a single formula, performing multiple steps is fine, and I thought there might be a way to re-arrange the steps including the matrix transforms to get the angle out. – Ben Slinger Aug 11 '19 at 22:21
  • @BenSlinger finally I have your formula ;) check the update – Temani Afif Aug 13 '19 at 23:26
  • Thankyou, that works! Really appreciate the time you gave me on this! – Ben Slinger Aug 14 '19 at 23:43