简体   繁体   中英

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!

I would consider a different way to find the formula without matrix calculation 1 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:

在此处输入图片说明

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 here 1 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM