【问题标题】:Why does order of transforms matter? rotate/scale doesn't give the same result as scale/rotate为什么转换的顺序很重要?旋转/缩放与缩放/旋转的结果不同
【发布时间】:2019-11-05 05:22:56
【问题描述】:

在梳理了SVG specification 以及诸如thisthis 之类的指南之后,我仍然难以准确理解链式转换的工作原理。

精选相关引述

当您将 transform 属性应用于 SVG 元素时,该元素 获取当前正在使用的用户坐标系的“副本”。

还有:

当转换被链接时,需要注意的最重要的事情 就是这样,就像 HTML 元素转换一样,每个 变换应用于坐标系之后的坐标系 由前面的变换变换而成。

还有:

例如,如果您要对元素应用旋转, 然后是翻译,翻译根据 新的坐标系,而不是最初的非旋转坐标系。

还有:

转换的顺序很重要。该序列 转换函数在 transform 属性中指定 是它们应用于形状的顺序。

代码

第一个矩形的当前坐标系被缩放,然后旋转(注意顺序)。第二个矩形的当前坐标系旋转,然后缩放。

svg {
  border: 1px solid green;
}
<svg xmlns="http://www.w3.org/2000/svg">
  <style>
    rect#s1 {
      fill: red;
      transform: scale(2, 1) rotate(10deg);
    }
  </style>
  <rect id="s1" x="" y="" width="100" height="100" />
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
  <style>
    rect#s2 {
      fill: blue;
      transform: rotate(10deg) scale(2, 1);
    }
  </style>
  <rect id="s2" x="" y="" width="100" height="100" />
</svg>

问题

我们知道,当我们链接变换时,会复制该元素使用的当前坐标系,然后按照指定的顺序应用变换。

当我们有一个已经缩放的用户坐标系,并且我们对其应用旋转时,矩形(如图所示)有效地倾斜(注意改变的角度)。如果我们以相反的方式进行这两个变换(旋转,然后缩放),则不会发生这种情况。

关于缩放的当前坐标系如何旋转的专家帮助,将不胜感激。我试图从技术(内部工作)角度理解为什么第一个矩形会发生倾斜。

谢谢。

【问题讨论】:

  • 嗯,它和 CSS 元素一样,这可能对你有帮助:stackoverflow.com/questions/51077632/… .... 如果你能够做一些数学来计算最终的矩阵,你会明白但老实说它可能会变得非常棘手
  • 旋转第一个矩形的当前坐标系,然后缩放(注意顺序)。 --> 不,坐标先缩放然后旋转,但是元素先旋转然后缩放
  • @temaniAfif 抱歉,这是一个错字。我现在正在修复它。
  • 这只是仿射变换几何,它与 SVG 无关,除非您使用 SVG 来显示坐标变换。如果您沿角度的边缘进行各向异性拉伸,则角度似乎不会改变,如果不改变,它会改变。这里没有“转换顺序”问题。
  • 我已经编辑了我的答案以插入更准确的细节和一些计算

标签: css svg css-transforms


【解决方案1】:

为了说明它是如何工作的,让我们考虑一个动画来展示缩放效果如何改变旋转。

.red {
  width:80px;
  height:20px;
  background:red;
  margin:80px;
  transform-origin:left center;
  animation: rotate 2s linear infinite;
}
@keyframes rotate {
  from{transform:rotate(0)}
  to{transform:rotate(360deg)}

}
<div class="container">
<div class="red">
</div>
</div>

正如您在上面看到的,旋转创建了一个完美的圆形。

现在让我们缩放容器看看有什么不同:

.red {
  width:80px;
  height:20px;
  background:red;
  margin:80px;
  transform-origin:left center;
  animation: rotate 5s linear infinite;
}
@keyframes rotate {
  from{transform:rotate(0)}
  to{transform:rotate(360deg)}

}
.container {
  display:inline-block;
  transform:scale(3,1);
  transform-origin:left center;
}
<div class="container">
<div class="red">
</div>
</div>

请注意,我们不再有圆,但现在是椭圆。就像我们画了一个圆圈,然后我们将它拉伸,这在我们的矩形内创建了倾斜效果。


如果我们做相反的效果,我们从缩放效果开始,然后应用旋转,我们不会有任何倾斜。

.red {
  width:80px;
  height:20px;
  background:red;
  margin:80px;
  animation: rotate 2s linear infinite;
}
@keyframes rotate {
  from{transform:scale(1,1)}
  to{transform:scale(3,1)}

}
.container {
  display:inline-block;
  transform:rotate(30deg);
  transform-origin:left center;
}
<div class="container">
<div class="red">
</div>
</div>

换种说法:应用旋转将在 X 轴和 Y 轴之间保持相同的比率,因此以后进行缩放时不会看到任何不良影响,但仅缩放一个轴会破坏比率,因此我们的形状看起来很糟糕当我们尝试应用旋转时。


如果您想了解有关如何链接变换以及如何计算矩阵的更多详细信息,可以查看此链接:https://www.w3.org/TR/css-transforms-1/#transform-rendering。它是关于 HTML 元素的,但正如 SVG 规范中所说的那样。

以下是相关部分:

转换是累积的。也就是说,元素在其父坐标系内建立其局部坐标系。

从用户的角度来看,一个元素有效地累积了其祖先的所有变换属性以及应用于它的任何局部变换


让我们做一些数学运算,看看这两种转换之间的区别。让我们考虑矩阵乘法,因为我们正在处理 2D 线性变换,为了简单起见,我们将在 ℝ² 上执行此操作1

对于scale(2, 1) rotate(10deg),我们将有

 |2 0|   |cos(10deg) -sin(10deg)|   |2*cos(10deg) -2*sin(10deg) |
 |0 1| x |sin(10deg) cos(10deg) | = |1*sin(10deg) 1*cos(10deg)  |

现在如果我们将此矩阵应用于(Xi,Yi),我们将获得(Xf,Yf),如下所示:

 Xf = 2* (Xi*cos(10deg) - Yi*sin(10deg))
 Yf =     Xi*sin(10deg) + Yi*cos(10deg)

注意Xf 是如何有一个额外的乘数,这是造成倾斜效果的罪魁祸首。就像我们改变了行为或Xf 并保留了Yf

现在让我们考虑rotate(10deg) scale(2, 1)

 |cos(10deg) -sin(10deg)|   |2 0|   |2*cos(10deg) -1*sin(10deg) |
 |sin(10deg) cos(10deg) | x |0 1| = |2*sin(10deg) 1*cos(10deg)  |

然后我们将有

 Xf =  2*Xi*cos(10deg) - Yi*sin(10deg)
 Yf =  2*Xi*sin(10deg) + Yi*cos(10deg)

我们可以将2*Xi 视为Xt,并且可以说我们旋转了(Xt,Yi) 元素,并且该元素最初是根据X 轴缩放的。


1CSS 也使用仿射变换(如翻译),因此使用ℝ²(笛卡尔坐标)不足以执行我们的计算,因此我们需要考虑 ℝℙ²(同构坐标)。我们之前的计算将是:

 |2 0 0|   |cos(10deg) -sin(10deg) 0|   |2*cos(10deg) -2*sin(10deg) 0|
 |0 1 0| x |sin(10deg) cos(10deg)  0| = |1*sin(10deg) 1*cos(10deg)  0|
 |0 0 1|   |0          0           1|   |0            0             1|

在这种情况下什么都不会改变,因为仿射部分是null,但是如果我们将翻译与另一个变换相结合(例如:scale(2, 1) translate(10px,20px)),我们将得到以下结果:

 |2 0 0|   |1 0 10px|   |2 0 20px|
 |0 1 0| x |0 1 20px| = |0 1 20px|
 |0 0 1|   |0 0 1   |   |0 0  1  |

Xf =  2*Xi + 20px;
Yf =  Yi + 20px;
1  =  1 (to complete the multiplication) 

【讨论】:

  • 没错。另一种说法是矩形在拉伸的“宇宙”中旋转。从矩形的角度来看,它是在绕圈旋转。但从我们的观点(宇宙之外)来看,这个圈子被拉长了。使圆看起来像一个椭圆,而矩形看起来是倾斜的。
  • 这很有帮助,Temani。我将通过你的数学并正确理解矩阵乘法,我认为这将为我解决它。我刚刚意识到的另一件事可能有助于我更好地理解发生了什么:我们首先创建一个缩放坐标系是否正确。然后,在该坐标系中,我们创建一个新的旋转坐标系。由于 x 坐标的距离是正常情况的两倍(没有比例),x 坐标以某种方式旋转了 y 坐标的两倍?我将根据您的示例深入研究数学。
  • @Magnus 是的,完全适用于我用动画说明的坐标系,其中旋转创建了一个椭圆,不再是一个圆,因为我们在一个缩放的坐标系内旋转(像保罗说的一个拉伸的宇宙) 所以我们有偏斜效应,因为 X 和 Y 的行为不再相同(就像你在公式中看到的额外乘数一样)
  • 变换与当前使用的坐标系相关,对吧?所以,如果 scale(2, 1) 给我们留下了一个新的坐标系 xs1,那么下一个链式变换,这里是 rotate(10),是创建 xs1 的副本,然后从 xs1 旋转?我认为这是正确的,只是想确定一下。
  • @Magnus 从技术上讲 n 链式变换将仅转换为一个矩阵变换,因此最后我们将使用该 one 转换。所以我们总是有一个独特的变换应用但是因为矩阵不是微不足道的,大多数用户不会真正理解数学我们试图通过说我们从坐标 X 开始,然后用第一个变换变换到 X1 来简单地解释这一点我们取 X1,我们使用第二个来获得 X2,...直到 Xf,
【解决方案2】:

Temani Afif 解释它的方式遵循每个变换跨越的坐标系。您从视口开始,每个连续的坐标系都是派生的,并位于画布上的不同位置。这些坐标系可能不是笛卡尔坐标系(“拉伸的宇宙”)。它们在 DOM 树中从外向内构造,在属性中链接时从左到右。

但是您也可以从内到外从相反的方向想象相同的变换:首先,您在其笛卡尔用户空间坐标系中绘制一个矩形,然后通过一系列比例、旋转等对其进行变换,直到在视口坐标系中绘制它时,它会变形为其他东西。

但是如果你用第二种方式看它,属性中的链式转换需要从右到左处理:transform: scale(2, 1) rotate(10deg) 表示取一个矩形,首先将其旋转 10 度,然后然后在水平方向缩放旋转的矩形。

简而言之,这两者是等价的:

  • 如果您在变换坐标系中绘制图形,请通过从左到右对这些坐标系应用变换来构造坐标系。
  • 如果您在原始坐标系中绘制变换后的图形,请通过从右到左对图形应用变换来构造图形。

【讨论】:

  • 谢谢,ccprog。我现在只需要了解变换函数是如何表示为矩阵的。我将与 Temani 联系。再次感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多