How to match 3D perspective of real photo and object in CSS3 3D transforms

Viewed 3928

The situation:

In my particular case I have 3D photoframe image and a 2D photo. And I want the 2D photo to match the 3D frame using CSS3 transform functions (Rotate, Scale, Skew).

The problem:

I wasn't able to precisely match the two using manual method aka typing rotation value and watching what it does.

Ideal solution #1

Online visual tool exists that lets you drag corners of the photo (like photoshop does) and It gives you the correct CSS3 transform values.

Ideal solution #2

Non-visual tool exist - same as before but you manually enter 4 point coordinates (image corners) and it gives you the correct CSS3 transform values.

Real solution of this question

If there aren't such tools (my search found none) I would like somebody to try explain the math behind it so I could calculate it myself - If it is even possible?

I prepared JSFiddle demo for you fiddle around: Demo

/* Main issue here */

.transform {
  transform: rotateX(34deg) rotateZ(13deg) rotateY(-10deg) scaleY(1) scaleX(1) skewY(0deg) skewX(0deg) translateY(0px) translateX(20px);
  transform-origin: 50% 0% 0;
}
/* Supporting styles */

.container {
  position: relative;
  width: 500px;
  height: 500px;
}
.frame,
.photo {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.photo {
  top: 50px;
  left: 95px;
  right: 65px;
  bottom: 270px;
}
.frame img,
.photo img {
  width: 100%
}
.frame {
  z-index: 2;
}
<div class="container">

  <div class="frame">
    <img src="http://cdn.idesigned.cz/img/cc08acc7b9b08ab53bf935d720210f13.png" />
  </div>

  <div class="photo">
    <div class="transform">
      <img src="https://static.pexels.com/photos/7976/pexels-photo.jpg" />
    </div>
  </div>

</div>

4 Answers

Based on @szym code I've created a demo for my needs that can be improved. The code uses draggable points so it's easier to find the points and calculate the matrix. Also the matrix is printed on the left.

// Computes the matrix3d that maps src points to dst.
function compute_transform(src, dst) {
  // src and dst should have length 4 each
  var count = 4;
  var a = []; // (2*count) x 8 matrix
  var b = []; // (2*count) vector

  for (var i = 0; i < 2 * count; ++i) {
    a.push([0, 0, 0, 0, 0, 0, 0, 0]);
    b.push(0);
  }

  for (var i = 0; i < count; ++i) {
    var j = i + count;
    a[i][0] = a[j][3] = src[i][0];
    a[i][1] = a[j][4] = src[i][1];
    a[i][2] = a[j][5] = 1;
    a[i][3] = a[i][4] = a[i][5] =
      a[j][0] = a[j][1] = a[j][2] = 0;
    a[i][6] = -src[i][0] * dst[i][0];
    a[i][7] = -src[i][1] * dst[i][0];
    a[j][6] = -src[i][0] * dst[i][1];
    a[j][7] = -src[i][1] * dst[i][1];
    b[i] = dst[i][0];
    b[j] = dst[i][1];
  }

  var x = numeric.solve(a, b);
  // matrix3d is homogenous coords in column major!
  // the z coordinate is unused
  var m = [
    x[0], x[3], 0, x[6],
    x[1], x[4], 0, x[7],
    0, 0, 1, 0,
    x[2], x[5], 0, 1
  ];
  return "matrix3d(" + m.join(',') + ')';
}

// Collect the four corners by user clicking in the corners:

var points = [];
// map flatten the array
$('.point').each(function() {
    var {left, top} = $(this).position();
    points.push([left, top]);
});

transform_terminal();

$('.point').each(function(i) {
    var drag = false;
    var [container] = $('.laptop');
    var $point = $(this).mousedown(function() {
        drag = true;
    });
    $(document).on('mouseup', function() {
        drag = false;
    }).on('mousemove', function(event) {
        if (drag) {
            var box = container.getBoundingClientRect();
            var x = event.clientX - box.left;
            var y = event.clientY - box.top;
            points[i] = [x, y];
            $point.css({
                left: x,
                top: y
            });
            transform_terminal();
        }
    });
});

function transform_terminal() {
    var w = gemetry.width + 20,
        h = gemetry.height + 20;
    var transform = compute_transform(
        [
            [0, 0],
            [w, 0],
            [w, h],
            [0, h]
        ],
        points
    );
    $('.output pre').html(`
.terminal {
    transform: ${transform};
}
    `.trim())
    term.css({
        '--transform': transform
    });
}

See demo in Action I use it to match jQuery Terminal into an image of a laptop.

Related