Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculate absolute dimensions of a div rotated in perspective with css3

lets say we have a div with 500x500px size and we rotate it on the x axis via css 45 degrees considering a webkit-perspective value of 1600px.

How would you calculate the absolute dimensions of the displayed trapezium? (width, max-height, angles)

I did only figure out a formula that calculates the width but without considering the perspective, so the value differ some pixels (JavaScript):

var absoluteWidth = Math.cos(45 * (Math.PI / 180)) * 500);

EDIT: Here is the spec about the -webkit-perspective function:

perspective(<number>)

specifies a perspective projection matrix. This matrix maps a viewing cube onto a pyramid whose base is infinitely far away from the viewer and whose peak represents the viewer's position. The viewable area is the region bounded by the four edges of the viewport (the portion of the browser window used for rendering the webpage between the viewer's position and a point at a distance of infinity from the viewer). The depth, given as the parameter to the function, represents the distance of the z=0 plane from the viewer. Lower values give a more flattened pyramid and therefore a more pronounced perspective effect. The value is given in pixels, so a value of 1000 gives a moderate amount of foreshortening and a value of 200 gives an extreme amount. The matrix is computed by starting with an identity matrix and replacing the value at row 3, column 4 with the value -1/depth. The value for depth must be greater than zero, otherwise the function is invalid.

Regarding the "perspective projection matrix" this is what I found on Wikipedia: http://en.wikipedia.org/wiki/3D_projection#Perspective_projection

like image 645
Elias Avatar asked Dec 27 '11 18:12

Elias


1 Answers

I get a headache with matrices, so I'm doing this with proportions.

If you see the div from above (hence seeing the rotation in the two dimensions it takes place in), you're seeing it as a segment on the xz plane, with coordinates (-250, 0) (250, 0), or in general (-w/2, 0) (w/2, 0) After a rotation on the y axis, the coordinates will become, similarly to what you stated

(-Math.cos(angle) * w/2, -Math.sin(angle) * w/2)
( Math.cos(angle) * w/2,  Math.sin(angle) * w/2)

, being the rotation counterclockwise, with the origin at the center of the div, and of angle radians.

Using the perspective means that these coordinates are not displayed just by discarding the z, but they are first projected according to their distance from the observer.

Now, the projection plane is the one where the unrotated things lay, with z = 0. I deduce this from the fact that when unrotated divs are projected, they remain the same size. If you take a point with distance p (the perspective value) from the z plane, so with xz coordinates (0, -p), and draw a line from this point to the vertices of the rotated segment, up to when it crosses the projection plan, the points you get are the new segment coordinates which yield the div final size.

With a proportion between the triangles (0, -p) (0, 0) (x, 0) and (0, -p) (0, sin*w/2) (cos*w/2, sin*w/2), you get that

p : x = (p + sin*w/2) : cos*w/2
x = (p * cos*w/2) / (p + sin*w/2)

which in general means that when you project the point (x, y, z) onto the plan you get

x * p / (p + z)
y * p / (p + z)
0

So your final div coordinates (on xz, relative to div's center) will be

(-Math.cos(angle) * w/2 * p / (p + -Math.sin(angle) * w/2), 0)
( Math.cos(angle) * w/2 * p / (p +  Math.sin(angle) * w/2), 0)

From which you can calculate its width but also its position - which is non trivial, since its nearest-to-the-viewer half will appear bigger than the other half.

Look at the following test for more details (it fails when you're too close to the objects, I'm not sure why, probably some variable overflows)

var WIDTH = 500;
var P = 300;
jQuery(function(){
function test(width, angle, p) {
    $('body').
        append($('<div id="info" />')).
        append($('<div id="container" />').
            css({
                margin: '50px 0px',
                border: '1px solid black',
                width: width+'px',
                '-webkit-perspective': p
            }).
            append($('<div id="real" />').addClass('the_div').css({ 'width': width+'px' }))).
        append($('<div id="fake" />').addClass('the_div'));

    setInterval(function() {
        angle += 1;

        $('#real').css({ '-webkit-transform': 'rotateY('+angle+'deg)' }).html(width);

        // initial coordinates
        var A = 0;
        var B = width;
        // translate the center (assuming -perspective-origin at 50%)
        A -= width/2;
        B -= width/2;
        // new coordinates
        A = calc(A, angle*Math.PI/180, p);
        B = calc(B, angle*Math.PI/180, p);
        // translate back
        A += width/2;
        B += width/2;
        if(B < A) { var tmp = A; A = B; B = tmp; } // swap
        var realwidth = B-A;
        $('#fake').html(width+'<br/>'+A+', '+B).css({
            'width': realwidth+'px',
            'margin-left': A+'px'
        });

        // shows debug information
        var debug = function(values) { return values.map(function(i){ return i+': '+eval(i); }).join('<br />'); }
        $('#info').html($('<div />').html(debug(['width', 'p', 'angle', 'A', 'B', 'realwidth'])));

    }, 40);
}

function calc(oldx, angle, p) {
    var x = Math.cos(angle) * oldx;
    var z = Math.sin(angle) * oldx;

    return x * p / (p+z);
}

test(WIDTH, 0, P);
});
* {
  margin: 0px;
  padding: 0px;
}
body {
  padding: 40px 100px;
}
.the_div {
  height: 100px;
  border: 2px solid black;
  background-color: rgba(255, 192, 0, 0.5);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Note that if you're not giving a perspective value, the result will be equal as having an infinite value for it.

like image 144
djjeck Avatar answered Oct 23 '22 01:10

djjeck