I was cleaning out some old notes from my previous job and found some math scribbles for computing CSS transforms and thought I would share it. For some context, I was working on a page which had an image that looked like this:

I wanted to add an easter egg where I could use the screens of those devices to display arbitrary things. I thought it would just be a simple matter of pixel pushing with translate/rotate/scale/etc using CSS transforms but couldn't really get things to line up perfectly.

Frustrated, I started trying to solve it analytically instead. That means for any given shape, I need to solve for the perspective transform that warps an element into that shape. Once that is solved, it is easy to write a WYSIWYG helper script for outputting the CSS. Here's the final result:

See the coffeescript tab for the code. Or paste this gist into console to try it out on any page that has jQuery. You will need to change the selector to whatever element you want to add the dots to.

Using that you can drag things into whatever (convex quadrilateral) shape you want:

I ended up not using this for anything but hope someone else will find it useful!

The rest of this post will explain how to derive the equation for the transform since I remember not being able to find much about it back then. Looking at the code you will see that the core logic is just a few lines to set up and solve a system of linear equations. We will now see how to derive that system.

So let's say we have the 4 corners of the element we want to transform, (xi,yi) where i0,1,2,3 and we want to map each (xi,yi) to some (ui,vi). From the docs of matrix3d, the transform we want is a homogeneous matrix so we have to represent each point using homogeneous coordinates. In homogenous coordinates, a point (x,y) is represented by (kx,ky,k) for any k0. For example (3,2,1) and (6,4,2) both represent the point (3,2).

Thus the transformation matrix H that we want to solve for must satisfy

(h0h1h2h3h4h5h6h7h8)H(xiyi1)=ki(uivi1)

for each i, where the knowns are xi,yi,ui,vi.

Notice that the H that can satisfy this is not unique. For example you can scale H by some constant and the resulting matrix will still map the points correctly (since you can also scale the ki by the same amount and still represent the same homogeneous point). So assuming h80 (see footnote[1]), we should always be able to scale both sides until h8=1, which will simplify the problem for us a little bit:

(h0h1h2h3h4h5h6h71)(xiyi1)=ki(uivi1)

Now we should try to get it into a form that we can solve. Multiplying out we get:

xih0+yih1+h2=kiuixih3+yih4+h5=kivixih6+yih7+1=ki

We can get rid of ki by substituting it from the third into the first two equations:

xih0+yih1+h2=uixih6+uiyih7+uixih3+yih4+h5=vixih6+viyih7+vi

Remember we are trying to solve for hi so we should try to separate them out:

xih0+yih1+h2uixih6uiyih7=uixih3+yih4+h5vixih6viyih7=vi

Which in matrix notation is:

(xiyi1000uixiuiyi000xiyi1vixiviyi)(h0h1h2h3h4h5h6h7)=(uivi)

Since we have 4 of these mappings we can write them like this:

(x0y01000u0x0u0y0000x0y01v0x0v0y0x1y11000u1x1u1y1000x1y11v1x1v1y1x2y21000u2x2u2y2000x2y21v2x2v2y2x3y31000u3x3u3y3000x3y31v3x3v3y3)(h0h1h2h3h4h5h6h7)=(u0v0u1v1u2v2u3v3)

At this point we are done because it is in Ah=b form so we can just throw this at a matrix algebra library to solve for h. It should spit back out the hi which will let us recover the transform we want:

H=(h0h1h2h3h4h5h6h7h8)

One last wrinkle is that matrix3d actually takes in a 4 by 4 matrix rather than a 3 by 3. Since we don't care about z values (because all our points are on the same plane, z=0) we can just make z map back to itself. Like so:

(h0h10h2h3h40h50010h6h70h8)

And that's the final matrix you use for matrix3d. Remember to specify it in column major order and also set the transform-origin to whatever you measured your points with respect to.

I didn't know what to google when I first did this so I had to derive this by hand. I have recently been reading a book on computer vision and it turns out this problem is actually a basic problem in that field (see getPerspectiveTransform in opencv) and the technique for solving this is called direct linear transformation.

EDIT (2016-11-09): As mentioned by Alexander in the comments, matrix3d used to be incorrect on Chrome under page zoom. I contributed a fix to chromium so it should be fine now.


  1. If you want to solve this rigorously without making the assumption that h80 you can still follow the same steps outlined in this post. You will just end up with a homogeneous system Ah=0 instead (where A is now a 8 by 9 matrix and h is a 9-vector). This can be solved by doing a singular value decomposition and then finding the singular vector corresponding with a singular value of zero. Then any scalar multiple of that singular vector will be a solution. The js library I was using for math didn't support SVD very well though! ↩︎