SVG 元素的自定义 transform origin

- hikerpig
#SVG

不要使用 transform-origin 属性

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

自行解释 transformOrigin

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

首先建立一个对象系统


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')
  }
}

此时有

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 的操作,得到

<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>

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

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

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_xT_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 计算方法改为:

   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倍,

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),再加上元素本身的位移,最后得到:

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

变换过程示意:

变换过程示意

参考