如何控制 `<use>` 元素的 `transform-b​​ox`?

How to control `transform-box` for `<use>` elements?

背景

我喜欢 SVG2 中扩展的 CSS 支持。不必一遍又一遍地重写属性真是太好了。所以我一直在将项目中的一些代码从 SVG 属性转换为 CSS。其中大部分工作正常。

当谈到转换时,如果您对 CSS 转换在 HTML 中的工作方式感到满意,事情可能看起来很棘手。 (对于 rotate() 转换尤其如此,这是本问题的重点。)这是因为 SVG 没有 HTML 具有的“自动流”。

换句话说,当你有一堆HTML个元素时,一个接一个,它们会自动按照盒子模型自行布局。

SVG 中没有这种“自动”或“默认”布局。因此,SVG 转换默认为从原点计算。 (即用户坐标中的 0,0)。


几乎-完美的解决方案

对于大多数元素,有一个简单的解决方案:很棒的 CSS 属性 transform-box。在大多数情况下,使用以下 CSS 将允许您以与 HTML 元素几乎相同的方式转换 SVG 元素:

/* whatever elements you want to transform */
rect, polygon {      
    transform-box:    fill-box;  
    transform-origin: center center;  /* or `top left`, `center right`, etc. */
}

.rotate90 {
    transform: rotate(90deg);
}

现在,您可以做类似...

<rect class="rotate90" x="123" y="789" width="50" height="50" />

并且会围绕CSS中指定的transform-origin旋转。由于上面的例子使用了 center centertransform-origin,它原地旋转。

这与使用 transform: rotate(…) 的 HTML 个元素的行为相匹配。而且——特别是如果在 SVG 图像中有很多像这样的旋转——它 way 比等效的 SVG 标记更好。

为什么 CSS 比等效的 SVG 标记更好?

因为 SVG’s rotate() function has a slightly different syntax 没有等同于上面使用的 transform-box: fill-box CSS,除非您为每个旋转指定 X 和 Y 坐标。

这意味着你必须每次输入旋转点,像这样:

<rect x="123" y="789" width="50" height="50" 
      transform="rotate(90 123 789)"
></rect>
<!-- 
  In this example, the rotation pivots around the X and Y coordinates of the `rect` element.
  
  If you wanted to rotate around the center, you would have to calculate:
  
  x + width/2
  y + width/2

  And use those values in the `rotate()` function instead.

 -->

问题

我 运行 遇到的问题是 CSS 解决方案不适用于 <use /> 元素。

很清楚为什么<use /> 元素将引用的元素克隆到新位置。因此,就 CSS 而言,它正在转换 <use /> 元素,而不是克隆的 (Shadow-DOM) 内容。

当涉及将 CSS 应用于 <use /> 的其他挑战时——例如设置不同的配色方案——有解决方案(例如 this one from SVG superhero Sara Soueidan)。

但是要解决坐标的问题,我还没有想出解决办法。

示例代码

编辑:为了更明确地说明我要做什么,这里有一些示例代码。

.transform-tl,
.transform-tc,
.transform-tr,
.transform-cl,
.transform-cc,
.transform-cr,
.transform-bl,
.transform-bc,
.transform-br {
    transform-box: fill-box;
}

.transform-tl { transform-origin: top left; }
.transform-tc { transform-origin: top center; }
/*  
…and so on, for the other combinations of `transform-origin` keyword values… 
*/

.rotate90.cw {
    transform: rotate(90deg)
}

.rotate90.ccw {
    transform: rotate(-90deg)
}
<!-- 
    Using the CSS classes in the following manner works as intended for most SVG elements as intended.
    
    But with the `<use />` element, the rotation does not pivot around the top left corner, as expected... :(
-->
<use 
    class="transform-tl rotate90 cw" 
    x    ="0" 
    y    ="1052" 
    href ="#block-A12-2"
></use>

(感谢@PaulLeBeau 推动包含这段代码。)


有人有解决办法吗?

(即使是变通解决方案——只要它涉及的重复次数少于在每个 <use /> 上指定 SVG transform 属性——也是受欢迎的!)

所以您想用 <use x="..." y="..." 将元素放在某个地方,然后将其旋转到位?

我找到的最简单的解决方案确实包括 transform 属性,但您不需要指定旋转点。请参阅以下示例,其中绿色矩形符合您的要求。

在CSS中,我们在transform-box规则中包含了use个元素。然后我们定位旋转每个具有transform属性的元素(替换xy和CSS旋转):

/* whatever elements you want to transform */
rect, polygon, use {
    transform-box: fill-box;
    transform-origin: center center; /* or `top left`, `center right`, etc. */
}

.rotate45 {
    transform: rotate(45deg);
}
<svg width="300" height="200">
    <rect id="rr" x="80" y="60" width="50" height="50" />

    <use href="#rr" x="100" y="0" class="rotate45"          fill="tomato" />
    <use href="#rr" transform="translate(100 0) rotate(45)" fill="lime" />
</svg>

假设与@Sphinxxx 假设的条件相同...

然后您也可以将 <use> 元素包装在一个组 (<g>) 元素中,然后对其应用旋转 class。

/* whatever elements you want to transform */
rect, polygon, use, g {
    transform-box: fill-box;
    transform-origin: center center; /* or `top left`, `center right`, etc. */
}

.rotate45 {
    transform: rotate(45deg);
}
<svg width="300" height="200">
    <defs>
      <rect id="rr" x="80" y="60" width="50" height="50" />
    </defs>

    <use href="#rr" x="100" y="0" fill="tomato" />
    <g class="rotate45">
      <use href="#rr" x="100" y="0" fill="lime" />
    </g>
</svg>

这是怎么回事?为什么这样做?

原因与取消引用 <use> 元素的方式以及发生这种情况时发生的转换有关。如果您阅读 the section about <use> elements in the SVG spec,它表示:

In the generated content, the ‘use’ will be replaced by ‘g’, where all attributes from the ‘use’ element except for ‘x’, ‘y’, ‘width’, ‘height’ and ‘xlink:href’ are transferred to the generated ‘g’ element. An additional transformation translate(x,y) is appended to the end (i.e., right-side) of the ‘transform’ attribute on the generated ‘g’, where x and y represent the values of the ‘x’ and ‘y’ attributes on the ‘use’ element.

所以,下面的 SVG(我想这会像你正在尝试的那样):

<use href="#rr" x="100" y="0" fill="lime" class="rotate45" />

将被取消引用为等同于:

<g fill="blue" transform="rotate(45) translate(100,0)">
  <rect x="80" y="60" width="50" height="50" />
</g>

因为你可以认为变换是从右到左应用的,所以 translate(100,0) 将在旋转之前应用,这意味着旋转将移动偏离你想要的位置 100 像素的东西它。这就是 transform-box: fill-box 不适用于 <use> 元素的原因。

rect, polygon, use, g  {
    transform-box: fill-box;
    transform-origin: center center; /* or `top left`, `center right`, etc. */
}

.rotate45 {
    transform: rotate(45deg);
}
<svg width="300" height="200">
    <defs>
      <rect id="rr" x="80" y="60" width="50" height="50" />
    </defs>

    <use href="#rr" x="100" y="0" fill="tomato" />

    <use href="#rr" x="100" y="0" fill="blue" stroke="blue" class="rotate45" />

    <g fill="lime" transform="rotate(45) translate(100,0)">
      <rect x="80" y="60" width="50" height="50" />
    </g>
</svg>

但是对于我的解决方案,<g> + <use> 设置...

<g class="rotate45">
  <use href="#rr" x="100" y="0" fill="lime" />
</g>

将被取消引用为等同于:

<g class="rotate45">
  <g fill="lime" transform="translate(100,0)">
    <rect x="80" y="60" width="50" height="50" />
  </g>
</g>

rect, polygon, use, g  {
    transform-box: fill-box;
    transform-origin: center center; /* or `top left`, `center right`, etc. */
}

.rotate45 {
    transform: rotate(45deg);
}
<svg width="300" height="200">
    <defs>
      <rect id="rr" x="80" y="60" width="50" height="50" />
    </defs>

    <use href="#rr" x="100" y="0" fill="tomato" />

    <g class="rotate45">
      <use href="#rr" x="100" y="0" fill="blue" stroke="blue"/>
    </g>

    <g class="rotate45">
      <g fill="lime" transform="translate(100,0)">
        <rect x="80" y="60" width="50" height="50" />
      </g>
    </g>
</svg>

在此版本中,两个变换操作中的每一个都应用于不同的元素,因此 transform-boxtransform-origin 分别应用。并且变换原点对平移没有影响。

这意味着 transform-box 旋转计算(即在 <g> 上)将应用于已翻译的对象。所以它会如你所愿。

transform-box 之前的日子里,这两个示例会给出相同的结果。