Search code examples
javascripthtmlcss3dcss-transforms

Generating CSS 3D Mockups


I was trying to create some laptop mockups from scratch with CSS 3D transforms (pen) and I noticed that it was harder than I thought. Here's the code I was messing with:

  transform: scaleX(1) scaleY(1) scaleZ(1) 
    rotateX(48deg) rotateY(-35deg) rotateZ(37deg)
    translateX(360px) translateY(103px) translateZ(0px) 
    skewX(0) skewY(0);

I haven't yet worked out how to do it, but I believe that if we can get the corners of the image on top of the corresponding corners of the laptop image, then it will look realistic. Am I wrong?

oops image broken

I think that this could be done with a function that takes 5 inputs (ratio of source image's rectangle and the coordinates of the four corners of the destination (laptop) image) and returns an object containing valid CSS properties for layering the image.

Would you have any insight on how it could be done?


Solution

  • This Mathematics question has the doorway:

    https://math.stackexchange.com/questions/296794/finding-the-transform-matrix-from-4-projected-points-with-javascript

    It leads to these nice live examples:

    Here's the pen from the article ported from CoffeeScript to this answer in plain JavaScript:

    // Original was CoffeeScript at https://codepen.io/fta/pen/ifnqH?editors=0010.
    // This is ported back to plain JavaScript. It could be cleaned up!
    {
      const $ = jQuery;
    
      function getTransform (from, to) {
        console.assert(from.length === to.length && to.length === 4);
          
        const A = []; // 8x8
        for (let i = 0; i < 4; i++) {
          A.push([from[i].x, from[i].y, 1, 0, 0, 0, -from[i].x * to[i].x, -from[i].y * to[i].x]);
          A.push([0, 0, 0, from[i].x, from[i].y, 1, -from[i].x * to[i].y, -from[i].y * to[i].y]);
        }
          
        const b = []; // 8x1
        for (let i = 0; i < 4; i++) {
          b.push(to[i].x);
          b.push(to[i].y);
        }
          
        // Solve A * h = b for h
        const h = numeric.solve(A, b);
        const H = [[h[0], h[1], 0, h[2]], [h[3], h[4], 0, h[5]], [0, 0, 1, 0], [h[6], h[7], 0, 1]];
          
    // Sanity check that H actually maps `from` to `to`
        for (let i = 0; i < 4; i++) {
          const lhs = numeric.dot(H, [from[i].x, from[i].y, 0, 1]);
          const k_i = lhs[3];
          const rhs = numeric.dot(k_i, [to[i].x, to[i].y, 0, 1]);
          console.assert(numeric.norm2(numeric.sub(lhs, rhs)) < 1e-9, "Not equal:", lhs, rhs);
        }
          
        return H;
      };
    
       function applyTransform(element, originalPos, targetPos, callback) {    
        // All offsets were calculated relative to the document
        // Make them relative to (0, 0) of the element instead
        const from = (function() {
          const results = [];
          for (let k = 0, len = originalPos.length; k < len; k++) {
            const p = originalPos[k];
            results.push({
              x: p[0] - originalPos[0][0],
              y: p[1] - originalPos[0][1]
            });
          }
          return results;
        })();
           
        const to = (function() {
          const results = [];
          for (let k = 0, len = targetPos.length; k < len; k++) {
            const p = targetPos[k];
            results.push({
              x: p[0] - originalPos[0][0],
              y: p[1] - originalPos[0][1]
            });
          }
          return results;
        })();
           
        // Solve for the transform
        const H = getTransform(from, to);
        
        // Apply the matrix3d as H transposed because matrix3d is column major order
        // Also need use toFixed because css doesn't allow scientific notation
        $(element).css({
          'transform': `matrix3d(${((function() {
            const results = [];
            for (let i = 0; i < 4; i++) {
              results.push((function() {
                const results1 = [];
                for (let j = 0; j < 4; j++) {
                  results1.push(H[j][i].toFixed(20));
                }
                return results1;
              })());
            }
            return results;
          })()).join(',')})`,
          'transform-origin': '0 0'
        });
           
        return typeof callback === "function" ? callback(element, H) : void 0;
      };
    
      function makeTransformable(selector, callback) {
        return $(selector).each(function(i, element) {
          $(element).css('transform', '');
          
          // Add four dots to corners of `element` as control points
          const controlPoints = (function() {
            const ref = ['left top', 'left bottom', 'right top', 'right bottom'];
            const results = [];
            for (let k = 0, len = ref.length; k < len; k++) {
              const position = ref[k];
              results.push($('<div>').css({
                border: '10px solid black',
                borderRadius: '10px',
                cursor: 'move',
                position: 'absolute',
                zIndex: 100000
              }).appendTo('body').position({
                at: position,
                of: element,
                collision: 'none'
              }));
            }
            return results;
          })();
            
          // Record the original positions of the dots
          const originalPos = (function() {
            const results = [];
            for (let k = 0, len = controlPoints.length; k < len; k++) {
              const p = controlPoints[k];
              results.push([p.offset().left, p.offset().top]);
            }
            return results;
          })();
          
          // Transform `element` to match the new positions of the dots whenever dragged
          $(controlPoints).draggable({
            start: () => {
              return $(element).css('pointer-events', 'none'); // makes dragging around iframes easier 
            },
            drag: () => {
              return applyTransform(element, originalPos, (function() {
                const results = [];
                for (let k = 0, len = controlPoints.length; k < len; k++) {
                  const p = controlPoints[k];
                  results.push([p.offset().left, p.offset().top]);
                }
                return results;
              })(), callback);
            },
            stop: () => {
              applyTransform(element, originalPos, (function() {
                const results = [];
                for (let k = 0, len = controlPoints.length; k < len; k++) {
                  const p = controlPoints[k];
                  results.push([p.offset().left, p.offset().top]);
                }
                return results;
              })(), callback);
                
              return $(element).css('pointer-events', 'auto');
            }
          });
            
          return element;
        });
      };
    
      makeTransformable('.box', function(element, H) {
        console.log($(element).css('transform'));
        return $(element).html($('<table>').append($('<tr>').html($('<td>').text('matrix3d('))).append((function() {
          const results = [];
          for (let i = 0; i < 4; i++) {
            results.push($('<tr>').append((function() {
              const results1 = [];
              for (let j = 0; j < 4; j++) {
                results1.push($('<td>').text(H[j][i] + ((i === j && j === 3) ? '' : ',')));
              }
              return results1;
            })()));
          }
          return results;
        })()).append($('<tr>').html($('<td>').text(')'))));
      });
    
    }
    .box {
      margin: 20px;
      padding: 10px;
      height: 150px;
      width: 500px;
      border: 1px solid black;
    }
    <div class="box">
        Drag the points to transform the box!
    </div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/numeric/1.2.6/numeric.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js"></script>