不要使用 transform-origin 属性

SVG 支持 transform,而且写法似乎与 CSS 中相同,但是它的标准里并不支持 transform-origin 属性。虽然在部分浏览器中,给 SVG 元素指定 transform-origin 似乎是有效果的(写法和结果也与 CSS 一样),但是无法指望这个行为在所有浏览器里都有效。

自行解释 transformOrigin

既然不能通过 attribute 来指定变换原点,我们只好通过对其他 transform 值的变动来实现想要的效果了。

首先建立一个对象系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

class Display {
x: number = 0
y: number = 0
width: number
height: number
scale: [number, number] = [1, 1]
transformOrigin?: [number, number]

parent?: Display
element: SVGElement

constructor() {
this.element = this.createElement() as any
}

createElement() {
return document.createElement('g')
}

addChild(child: Display) {
child.parent = this
this.element.appendChild(child.element)
}
}

class Rect extends Display {
createElement() {
return document.createElement('rect')
}
}

此时有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const r1 = new Rect({
name: "r1",
x: 10,
y: 10,
width: 100,
height: 50,
})

const r2 = new Rect({
name: "r2",
x: 10,
y: 10,
width: 100,
height: 50,
scale: [2, 2],
})

经过简单的属性到 dom 的操作,得到

1
2
3
4
<svg>
<rect name="r1" transform="translate(10,10)" width="100" height="50" fill="blue" opacity="0.8" />
<rect name="r2" transform="translate(10,10) scale(2,2)" width="100" height="50" fill="red" opacity="0.8" />
</svg>
<rect name="r1" transform="translate(10,10)" width="100" height="50" fill="blue" opacity="0.8" />

r2 的变换,先平移再缩放,平移的结果就是缩放的原点。

此处将 x/y 转为 translate 而不是 xy 属性,是为了以统一的方式做坐标系的转换和运算,且考虑到许多元素没有 xy 属性(如 circle 就只有 cxcy ),但所有 SVG 元素都支持 transform 。

1
2
3
4
5
6
function formTransform(d: Display) {
const scales = d.scale
const scaleX = scales[0]
const scaleY = scales[1]
return `translate(${d.x},${d.y}) scale(${scaleX},${scaleY})`
}

带位移补偿的缩放

计算缩放的位移补偿值,使得缩放再位移后效果就与以变换原点为中心缩放一样。

假设在缩放系数为 S 时,我们需要的 translate 为 \(TR\),变换完的结果:

\[ x'=(x+TR_x)\times S_x\\\\ y'=(y+TR_y)\times S_y \]

当以变换原点为特征点时,方程易于构建与求解。

\(T_x\)\(T_y\) 为变换原点相对于原坐标系左上角的坐标,当 \(x=T_x,\ y=T_y\) 时,代入得到:

\[ TO_{x} \ \ =\ ( TO_{x} \ +TR_{x}) \times S_{x}\\\\ TO_{y} \ \ =\ ( TO_{y} \ +TR_{y}) \times S_{y} \]

所以:

\[ TR_{x} \ =\frac{( 1-S_{x}) \times TO_{x}}{S_{x}}\\\\ \\\\ TR_{y} \ =\frac{( 1-S_{y}) \times TO_{y}}{S_{y}} \]

带缩放修正值的 transform 计算方法改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   const scales = d.scale
const scaleX = scales[0]
const scaleY = scales[1]
- return `translate(${d.x},${d.y}) scale(${scaleX},${scaleY})`
+
+ let xToOrigin = d.width / 2
+ let yToOrigin = d.height / 2
+ if (d.transformOrigin) {
+ xToOrigin = d.transformOrigin[0]
+ yToOrigin = d.transformOrigin[1]
+ }
+ const revisedX = (1 - scaleX) * xToOrigin
+ const revisedY = (1 - scaleY) * yToOrigin
+
+ return `translate(${d.x},${d.y}) scale(${scaleX},${scaleY}) translate(${revisedX},${revisedY})`
}

例如一个缩放为2倍,

1
2
3
4
5
6
7
8
const r3 = new Rect({
x: 50,
y: 50,
width: 100,
height: 50,
scale: [2, 2],
transformOrigin: [50, 25],
})

对应于缩放的变换应该是 scale(2,2) translate(-25,-12.5),再加上元素本身的位移,最后得到:

1
2
3
<svg>
<rect name="r3" transform="translate(50,50) scale(2,2) translate(-25,-12.5)" width="100" height="50"/>
</svg>

变换过程示意:

变换过程示意

参考