javascriptmatrixgsapskew

Is it possible to calculate a matrix transform function with given points?


I have a div that i want to project on a svg path. This path has 4 points (just as a dom-element) but is not a rectangle.

I've tried GSAP FLIP but this transforms to the boundingbox of the path.

https://codepen.io/KoentjeV/pen/mdQNgLr

 <svg fill="none" xmlns="http://www.w3.org/2000/svg" id="thesvg" width="500" height="500" viewbox="0 0 500 500">

    <path d="M0 103.85 L0 335.11L199.29 231.26 L199.29 0L0 103.85Z" fill="#6B8EFF" id="left" />
    <path d="M201.29 231.26L400.57 335.11V103.85L201.29 0V231.26Z" fill="#1242FE" id="right" />

    <path d="M1 336.11L200.29 232.26L399.57 336.11L200.29 431.56L1 336.11Z" fill="#5073FF" id="bottom" />

  </svg>

  <div class="toBeTransformed"></div>
.toBeTransformed {
  width: 300px;
  height: 300px;
  position: absolute;
  bottom: 50px;
  right: 50px;
  background: linear-gradient(180deg, #ffff00 0%, #e5e5e5 100%);
}

Flip.fit(".toBeTransformed", "#bottom", {
  scale: true,
  duration: 1.5,
  delay: 1,
  repeat: -1,
  yoyo: 1
});


Solution

  • All credits to MvG's answer: "Finding the Transform matrix from 4 projected points (with Javascript)" on Math Exchange. I can't remotely explain the math behind.

    The function explained will calculate a 3D matrix according to the 4 defined projection points and the target element's dimensions.

    I've slightly modified the script to work with projection points in clockwise order. (original function expects an order of: top-left, top-right, bottom-left, bottom-right).

    /**
     * based on "Finding the Transform matrix from 4 projected points (with Javascript)"
     * @MvG's answer
     * https://math.stackexchange.com/questions/296794/finding-the-transform-matrix-from-4-projected-points-with-javascript#339033
     * see original fiddle:
     * http://jsfiddle.net/zbh98nLv/
     */
    
    // retrieve coordinates from polygon
    let projectionRect = document.getElementById('bottom');
    let projectionPoints = projectionRect.points;
    let targetEl = document.getElementById('box');
    
    // set transform origin
    targetEl.style.transformOrigin = '0 0';
    
    // calculate matrix
    let matrix3d = getMatrix3dFromPoints(targetEl, projectionPoints);
    
    // apply matrix transform
    targetEl.style.transform = `matrix3d(${matrix3d.join(', ')})`;
    
    
    
    // calculate 3d transform matrix
    function getMatrix3dFromPoints(el, pts) {
      let [x1, y1, x2, y2, x4, y4, x3, y3] = [pts[0].x, pts[0].y, pts[1].x, pts[1].y, pts[2].x, pts[2].y, pts[3].x, pts[3].y]
      let w = el.offsetWidth,
        h = el.offsetHeight;
      let t = general2DProjection(0, 0, x1, y1, w, 0, x2, y2, 0, h, x3, y3, w, h, x4, y4);
      for (let i = 0; i != 9; ++i) {
        t[i] = t[i] / t[8];
      }
    
      let matrix3D = [t[0], t[3], 0, t[6],
        t[1], t[4], 0, t[7],
        0, 0, 1, 0,
        t[2], t[5], 0, t[8]
      ];
    
      return matrix3D;
    }
    
    
    function general2DProjection(
      x1s, y1s, x1d, y1d,
      x2s, y2s, x2d, y2d,
      x3s, y3s, x3d, y3d,
      x4s, y4s, x4d, y4d
    ) {
      let s = basisToPoints(x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s);
      let d = basisToPoints(x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d);
      return multmm(d, adj(s));
    }
    
    
    function basisToPoints(x1, y1, x2, y2, x3, y3, x4, y4) {
      let m = [x1, x2, x3, y1, y2, y3, 1, 1, 1];
      let v = multmv(adj(m), [x4, y4, 1]);
      return multmm(m, [
        v[0], 0, 0,
        0, v[1], 0,
        0, 0, v[2]
      ]);
    }
    
    // multiply matrix and vector
    function multmv(m, v) {
      return [
        m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
        m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
        m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
      ];
    }
    
    
    // multiply two matrices
    function multmm(a, b) {
      let c = Array(9);
      for (let i = 0; i != 3; ++i) {
        for (let j = 0; j != 3; ++j) {
          let cij = 0;
          for (let k = 0; k != 3; ++k) {
            cij += a[3 * i + k] * b[3 * k + j];
          }
          c[3 * i + j] = cij;
        }
      }
      return c;
    }
    
    // Compute the adjugate of m
    function adj(m) {
      return [
        m[4] * m[8] - m[5] * m[7], m[2] * m[7] - m[1] * m[8], m[1] * m[5] - m[2] * m[4],
        m[5] * m[6] - m[3] * m[8], m[0] * m[8] - m[2] * m[6], m[2] * m[3] - m[0] * m[5],
        m[3] * m[7] - m[4] * m[6], m[1] * m[6] - m[0] * m[7], m[0] * m[4] - m[1] * m[3]
      ];
    }
    svg {
      position: absolute;
      top: 0px;
      left: 0px;
    }
    
    #box {
      position: absolute;
      top: 0px;
      left: 0px;
      width: 200px;
      height: 200px;
    }
    
    .toBeTransformed {
      width: 300px;
      height: 300px;
      position: absolute;
      left: 0;
      top: 0px;
      background: linear-gradient(180deg, #ffff00 0%, red 100%);
      opacity: 1;
    }
    <svg fill="none" xmlns="http://www.w3.org/2000/svg" id="thesvg" width="500" height="500" viewbox="0 0 500 500">
        <path d="M0 103.85 L0 335.11L199.29 231.26 L199.29 0L0 103.85Z" fill="#6B8EFF" id="left" />
        <path d="M201.29 231.26L400.57 335.11V103.85L201.29 0V231.26Z" fill="#1242FE" id="right" />
        <polygon points="1 336.11 
                    200.29 232.26 
                    399.57 336.11
                    200.29 431.56
                    1 336.11 " fill="#5073FF" id="bottom" />
      </svg>
    
    <div id="container">
      <div id="box" class="toBeTransformed"></div>
    </div>

    To get the required 4 projection points I converted the <path> to a <polygon>.

    This way we can easily get all points as a point array

    let projectionRect = document.getElementById('bottom');
    let projectionPoints = projectionRect.points;
    

    3D transforms are not supported for svg elements

    In your case this is not a huge deal as you're projecting a HTMl element.