【问题标题】:Get "absolute" CSS transformation matrix获取“绝对”的 CSS 转换矩阵
【发布时间】:2021-10-16 07:35:38
【问题描述】:

我正在实现一组将像这样使用的自定义元素:

<om-root>
  ...
  <om-node id="node1">
    ...
    <om-node id="node2">
      ...
    </om-node>
    ...
  </om-node>
  ...
<om-root>

也就是说,我的 &lt;om-node&gt; 元素将与任意 HTML 混合,其中可能应用了定位和/或 CSS 转换。

这些&lt;om-node&gt; 元素的目的是根据各种条件对其内容应用 CSS 仿射变换。但无论其在层次结构中的位置如何,每个om-node 都会计算相对于根节点的变换。

我不能只将计算出的转换应用于节点,因为浏览器会将其与其所有祖先元素的转换结合起来:如果我将 node1 旋转 30 度,那么 node2 也将旋转 30 度在应用其自身的变换之前 30 度。

理想情况下,我想要的是类似于Element.getClientRects() 的东西,但返回一个矩阵而不仅仅是一个边界框。然后我可以做一些数学运算来补偿&lt;om-node&gt;&lt;om-root&gt; 元素的坐标系之间的差异。

This question 与我的相似,但没有有用的答案。问题提到了使用getComputedStyle(),但这并没有起到所声称的作用——getComputedStyle(elt).transform 返回相对于元素包含块的转换,不是视口。另外,结果不包括“传统”CSS 定位的效果(事实上,对于传统定位的元素,它根本没有价值)。

所以: 是否有可靠的方法来获取元素相对于视口的转换矩阵?

布局引擎显然有这个信息,我不希望每次发生任何变化时都执行复杂(且昂贵)的树遍历过程。

【问题讨论】:

  • 如果您可以访问 chrome devtools API(例如,使用一些电子应用程序、chrome 调试器等),那么有一个 API 可以帮助解决这个问题DOM.getContentQuadschromedevtools.github.io/devtools-protocol/tot/DOM/…
  • 我一般需要在网络上进行这项工作,这样 API 就不能直接使用。但这让我想到:可能有一种方法可以仅使用 getClientRects() 来计算元素的变换四边形(以及仿射变换矩阵)......

标签: javascript css dom layout


【解决方案1】:

CSS 转换实际上是相对繁重的操作,并且确实带有一些陷阱..(它们是 TRANSFORM 元素),因此您可能无法避免在不实现智能状态系统的情况下遍历节点,例如,将所有对象 + 转换存储在您的 javascript 类中..

也就是说,对于小型用例,一种简单的解决方法是禁用所有父元素 using something like 'inline' 的转换,但这并不适用于所有情况..

<div id="outside">
    <div id="inside">Absolute</div>
</div>

document.getElementById('outside').style.display = "inline";

更健壮的方法是检索和解析computedStyles dynamically ...

function getTranslateXY(element) {
    const style = window.getComputedStyle(element)
    const matrix = new DOMMatrixReadOnly(style.transform)
    return {
        translateX: matrix.m41,
        translateY: matrix.m42
    }
}

然后您可以通过在当前转换状态中添加/减去来在任何节点上动态设置新的转换。

【讨论】:

  • 是的,如果我只是担心翻译不会太难处理(尽管“常规”CSS 定位和通过 CSS 转换的翻译是分开报告的,所以它比你的要复杂一点代码建议两者是否都在使用中)。但是我的自定义元素基本上会产生任意变换,包括缩放、旋转和剪切组件;而且我还希望它们能够与用户可能在两者之间应用的任何 CSS 正常工作。除非有它的 API,否则处理一般情况似乎非常复杂(而且可能很慢)。
  • (无论如何我还是授予了赏金,因为你是唯一回答的人......)
【解决方案2】:

对这个问题进行了更多思考后,我突然想到,事实上,您可以使用getBoundingClientRect()解决问题。

当然,getBoundingClientRect() 本身并不能告诉您元素是如何被转换的,因为同一个边界框描述了许多可能的转换:

但是,如果我们添加三个具有已知大小和相对于父级的位置的子元素,那么我们可以通过比较它们的每个边界框来找出更多信息。下图显示了我放置这三个“仪表”元素的位置(实际上它们是空的且不可见的):

向量是父元素未变换坐标系的正交单位向量。元素经过各种CSS定位和变换属性变换后,首先需要找到变换后的单位向量u̅'v̅'。我们可以通过比较三个仪表元素的边界框来做到这一点——下图显示了具有两个不同示例转换的过程:

  • 从框 1 到框 2 的向量等价于 u̅'
  • 从框 1 到框 3 的向量等价于 v̅'
  • [框 3 的左上角] 和 [框 2 的右下角] 之间的中点给出了点 P:这是父元素原点的变换位置

从这三个值u̅'v̅'P我们可以直接构造一个二维仿射变换矩阵T强>:

这个矩阵T代表所有影响父元素的转换——不仅仅是CSS transform规则,还有“传统”定位,边距和边框的影响等等。而且,因为它是从 getBoundingClientRect() 计算出来的,所以它始终是相对于视口的——您可以直接比较任意两个元素,而不管它们在 DOM 层次结构中的关系如何。

注意:所有这些都假设我们只处理 2D 仿射变换,例如 transform:rotate(30deg)left:120px。处理 3D CSS 变换会更复杂,留给读者作为练习。

将以上代码化为代码形式:

    class WonderDiv extends HTMLElement {
        constructor () {
            super();
            this.gauges = [null, null, null];
        }
        connectedCallback () {
            this.style.display = "block";
            this.style.position = "absolute";
        }
        createGaugeElement (i) {
            let g = document.createElement("div");
            // applying the critical properties via a style
            // attribute makes them harder to override by accident
            g.style = "display:block; position:absolute;"
                + "margin:0px; width:100px; height:100px;"
                + "left:" + ( ((i+1)%2) ? "-100px;" : "0px;")
                + "top:" + ( (i<2) ? "-100px;" : "0px;");
            this.appendChild(g);
            this.gauges[i] = g;
            return g;
        }
        getClientTransform () {
            let r = [];
            let i;
            for (i=0; i<3; i++) {
              // this try/catch block creates the gauge elements
              // dynamically if they are missing, so (1) they aren't
              // created where they aren't needed, and (2) they are
              // restored automatically if someone else removes them.
              try { r[i] = this.gauges[i].getBoundingClientRect(); }
              catch { r[i] = this.createGaugeElement(i).getBoundingClientRect(); }
            }
            // note the factor of 100 here - we've used 100px divs
            // instead of 1px divs, on a hunch that might be safer
            return DOMMatrixReadOnly.fromFloat64Array(new Float64Array([
                (r[1].left - r[0].left) / 100,
                (r[1].top - r[0].top) / 100,
                (r[2].left - r[0].left) / 100,
                (r[2].top - r[0].top) / 100,
                (r[1].right + r[2].left) /2,
                (r[1].top + r[2].bottom) /2
            ]));
        }
    }
    
    customElements.define("wonder-div", WonderDiv);

– 自定义&lt;wonder-div&gt; 元素扩展&lt;div&gt; 以具有getClientTransform() 方法,其工作方式类似于getClientBoundingRect(),只是它返回DOMMatrix 而不是DOMRect

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多