3D CSS3 transforms handling with DOMMatrix issue
Asked Answered
B

1

1

Context

I'm trying to handle geometric operations applied to HTML elements by CSS3 3d transforms through DOMMatrix API to retrieve any coord i want in any coord system i want.

The Issue

Everything work fine with 2d transforms (translate, rotate, scale, skew) but when i start working in 3d with for exemple the "perspective" transform or a "rotateX" transform then nothing no longer work.

And i don't well understand why because in fact everything should be handled by the DOMMatrix operations like successives multiplications etc ..

Small exemple

// 2D working transforms --->
//const selfTransform = 'rotate(25deg) skewY(50deg) scale(0.6)';
// const selfTransform = 'rotate(45deg) skewX(50deg) scale(0.6)';
// <---

// 3D not working transforms --->
const selfTransform = 'perspective(500px) rotateX(60deg) rotateY(15deg)';
// <---

const DOM_a = document.getElementById('a');
DOM_a.style.transform = selfTransform;

// Generate the transform string
const transform = 
  // Translate to #a 0, 0
  'translate3d(100px, 100px, 0px) '
  // Translate to #a center, center
  + 'translate3d(100px, 100px, 0px) '
  // Apply #a transform
  + selfTransform
  // Translate back to #a 0, 0
  + 'translate3d(-100px, -100px, 0px)';

// Create the 3d projective transform matrix
const tm = new DOMMatrix(transform);

// Create the point representing the #a bottom left point coords in #A landmark
const blCoordsInA = new DOMPoint(
  0,
  200
);

// Find the #a bottom left point coords in the window landmrk by applying the transform
const blCoordsInWindow = blCoordsInA.matrixTransform(tm);
console.log(blCoordsInWindow);

const blLeft = blCoordsInWindow.x;
const blTop = blCoordsInWindow.y;


// Visualize the point by moving the black circle
const DOM_marker = document.getElementById('marker');
DOM_marker.style.left = blLeft + 'px';
DOM_marker.style.top = blTop + 'px';
#a {
  position: absolute;
  top: 100px;
  left: 100px;
  
  width: 200px;
  height: 200px;
  
  background-color: red;
  transform-origin: center center;
}

#marker {
  position: absolute;
  top: 0px;
  left: 0px;
  
  width: 6px;
  height: 6px;
  margin-left: -3px;
  margin-top: -3px;
  
  background-color: black;
  border-radius: 50%;
}
<div id="a">
</div>

<div id="marker">
</div>

As you can see in this exemple i'm trying to find the coordinates of the bottom left corner of the red div in the "general window landmark". Knowing the transform of the red div and the position of the point in the red div i normally should be able to find out the coords of thhis point in the window just by multiplying the coordinates of the point by the transform matrix i generate.

In my exemple if you define a 2D transform for the var "selfTransform" everything work as it should, the black dot is positionned on the bottom left corner. But if you set a 3d transform with some perspective then it's no longer working and i don't get why.

Any idea ?

Plot twist

In this exemple you can find out that some 3d transforms work well like "rotateX({randomRadianValue})" but it no longer work at the moment where there are cumulative transforms in the DOM tree, i mean if i want to retrieve the coords of a point that is in a div that also is in a tree where other div have 3d transforms all the results are fucked up. But if i'm only using 2d transforms then everything work perfeclty regardless of the depth of the element in the transformed element tree...

Bowen answered 10/6, 2024 at 8:29 Comment(0)
B
1

One thing that gets in the way is "where your corner points are". If you have a 200x200 rect and you're rotating over the center then corners (as far as the 3D transform is concerned) aren't (0,0), (0,200), (200,200), and (200,0), but instead are (-100,-100), (-100,100), (100,100), and (100,-100).

So if we capture those transform origin values before we do anything, and then make sure to work those offset into the position for our marker point, things work a lot better:

// capture our "starting offset":
const [tx=0, ty=0, tz=0] = getComputedStyle(plane)[`transform-origin`]
  .split(` `)
  .map(parseFloat);

function redraw() {
  a = angle.value;
  label.textContent = a;
  // Set up a transform...
  const transform = `
    perspective(250px)
    rotateX(${2*a  }deg)
    rotateY(${  a/3}deg)
    rotateZ(${  a}deg)
  `;

  // And get the matrix equivalent.
  const M = new DOMMatrix(transform);

  // Apply the transform to our plane...
  plane.style.transform = transform;
  
  // Draw all four corner points
  document.querySelectorAll(`.point`).forEach(p => p.remove());
  [[-tx, -ty], [-tx,ty], [tx,ty], [tx,-ty]].forEach(coords => {
    // First, transform the 2D corner point into a 4D homogeneous coordinate:
    const { x, y, z, w } = M.transformPoint(new DOMPoint(...coords, -tz));

    // then create a div that we're going to place at the
    // corner point's projected location:
    const p = document.createElement(`div`);
    p.classList.add(`point`);
    content.appendChild(p);

    // x and y (as well as z but we won't use that) are
    // still homogeneous values, so we need to perform a
    // homogeneous divide to turn them into "real" x and y,
    // and we also need to remember to compensate for
    // the fact that (0,0) is actually at (tx, ty).
    const { style } = p;
    style.setProperty(`--x`, `${x/w + tx}px`);
    style.setProperty(`--y`, `${y/w + ty}px`);
  });
}

angle.addEventListener(`input`, () => redraw());
redraw();
body {
  padding: 2em;
  padding-left: 4em;
  div:has(#slider) {
    margin-bottom: 2em;
    #slider {
      position: relative;
      margin-right: 14em;
      #angle {
        position: absolute;
        width: 15em;
      }
      label[for="angle"] {
        position: absolute;
        top: 1.5em;
        left: 7em;
      }
    }
  }
  #content {
    position: relative;
    width: 200px;
    height: 200px;
    background: yellow;
    #plane {
      position: absolute;
      width: 200px;
      height: 200px;
      background-color: red;
    }
    .point {
      position: absolute;
      --x: 0px;
      --y: 0px;
      top: calc(var(--y) - 3px);
      left: calc(var(--x) - 3px);
      width: 6px;
      height: 6px;
      background-color: black;
      border-radius: 50%;
    }
  }
}
<div>
  0 degrees
  <span id="slider">
      <label id="label" for="angle">0</label>
      <input id="angle" type="range" min="0" max="360" step="0.1" value="25">
    </span> 360 degrees
</div>

<div id="content">
  <div id="plane"></div>
</div>

But do note that things like CSS padding and margin can really mess with the placement of those points so you generally want to make sure that you're working in a "sterile" parent container with respect to those.

Bowler answered 10/6, 2024 at 17:7 Comment(7)
Considering the transform of the div : "perspective(500px) rotateX(25deg)". And a DOMMatrix constructed by doing "new DOMMatrix(perspective(500px) rotateX(25deg)). I know that the bottom left corner of the div has for coordinates : (0, 200, 0) in the div coordinates system. I want to retrieve the point coordinates in the window coordinates system, what calcul should i do ? new DOMPoint(0, 200, 0).matrixTransform(M) or new DOMPoint(0, 200, 0).matrixTransform(M.inverseSelf()) ? Because in my code i'm actually doing P.matrixTransform(M) and it works perfectly with 2D transforms..Bowen
I misunderstood you wrt the mapping, your (0,200,0) was already that starting screen coordinate so yes, that's a multiplication by the regular matrix, not its inverse. I've updated the post and deleted the now-irrelevant comments based on the old answer.Ambassadoratlarge
I perfectly understand your exemple everything seems clear.. but still i'm missing something, and when i try to get the bottom right point coordinates as the black circle by modifying line "const ttp = new DOMPoint(tx / 2, ty / 2, -tz);" in "const ttp = new DOMPoint(tx , ty, -tz);" which for me give the coordinates of the bottom right corner, nothing no longer works .. arh i feel kinda stupid, would you mind taking some time to explain me why and how ? maybe on another platform ? I'll buy you a coffee for the time spent ..Bowen
Also if i add a "translateX(250px)" (random translate value isn't important) in the transform string before the "perspective" nothing no longer work too and that is the same i don't get why because the matrix should be handling that translation "from my point of view"Bowen
I don't think it's you: I've updated the code to show this working for all four corner points... provided we don't add in a perspective instruction. Once we do, things stop working - which they shouldn't, since it's just a skew factor in m43, but they do. I'll try to see if I can figure that one out, because that's just plain weird.Ambassadoratlarge
Wow, dumb mistake: I forgot to destructure the w component, which is the perspective-related scaling factor (the "homogeneous divisor"). Doing so, and then remembering to use x/w and y/w gives the correct result. I've updated the code and it should do exactly what you expect now.Ambassadoratlarge
Damn thank you a lot ! the trick that make it works for me was just to divide each points x and y values by the point.w. I didn't know about that "perspective" factor directly in the DOMPoint attributes !Bowen

© 2022 - 2025 — McMap. All rights reserved.