在 SVG 上使用 skewX 和 skewY 时如何补偿平移
How to compensate translate when skewX and skewY are used on SVG
CONTEXT: @AnaTudor (@daredevil) is talking about 当 skewX
或 [=15= 时 SVG 的 d
移动距离] 被使用,所以我想知道是否有任何方法可以计算该距离以补偿平移,并避免使用链式 translate
.
测试用例:在下面的代码片段中,我们不会使用 SVG 特定链接 and/or 嵌套,我们只使用相同的变换值,但按照特定的顺序变换函数;
- 左侧绿色矩形 通过
style
:translate
、rotate
、skewX
、skewY
、scale
;
- 右橄榄矩形 通过
transform
属性:translate
、scale
、rotate
、skewX
、 `偏斜。
现在,如您所见,两个矩形的位置不同,如果您单击按钮,第二个矩形会更接近我们的预期,但仍然需要针对所有情况进行更多计算。
问题:我们如何改变fixOrigin
函数来调整所有可能的变换函数组合的翻译,以一种看起来与[=63相同的方式=]变换?
var el1 = document.querySelectorAll('path')[0],
el2 = document.querySelectorAll('path')[1],
el2BB = el2.getBBox(), el2cx = el2BB.x + el2BB.width/2, el2cy = el2BB.y + el2BB.height/2,
btn = document.querySelectorAll('button')[0], btn1 = document.querySelectorAll('button')[1],
x = 20, y = 20, scale = 0.6, rotate = 45, skewX = 20, skewY = -20;
el1.style.transform = 'translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6)';
el1.style.transformOrigin = '50% 50% 0px';
el2.setAttribute('transform', 'translate('+x+','+y+') scale('+scale+') rotate('+rotate+' '+el2cx+','+el2cy+') skewX('+skewX+') skewY('+skewY+')');
function fixOrigin(){
x += (1-scale) * el2BB.width/2;
y += (1-scale) * el2BB.height/2;
el2.setAttribute('transform', 'translate('+x+','+y+') scale('+scale+') rotate('+rotate+' '+el2cx+','+el2cy+') skewX('+skewX+') skewY('+skewY+')');
}
btn.addEventListener('click',fixOrigin,false);
function fixEverything() {
// scale binds all transform functions together
if ( !!scale ) {
//most important make sure we have translation values
//!!(x) && (x=0); !!(y) && (y=0);
// first adjust translate based on scale value
x += (1-scale) * el2BB.width/2;
y += (1-scale) * el2BB.height/2;
//now we also adjust the rotation transform origin based on SKEWS
if (!!rotate) {
// el2cx += .... el2cy += ...
}
//almost there, now we adjust the translation based on SKEWS
// x += ... y += ...
// last case, when SKEWS are used alone
} else if ( !scale && !rotate ) {
// adjust translation here
// x += ... y += ...
}
el2.setAttribute('transform', 'translate(' + x + ',' + y + ') scale(' + scale + ') rotate(' + rotate + ' ' + el2cx + ',' + el2cy + ') skewX(' + skewX + ') skewY(' + skewY + ')');
}
btn1.addEventListener('click', fixEverything, false);
/* desired transform
transform-origin: 50% 50% 0px;
transform: translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6);
*/
svg {
overflow: visible; width:30%;
border: 1px solid #eee; /* some sort of ruler */
}
<button>Fix Transform Origin</button><button>Fix All</button><br>
<p>Click to change the `transform` attribute</p>
<svg id="svgMixedCSS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="green" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z" ></path>
</svg>
<svg id="svgMixedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="indigo" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
我制作了一个超酷的片段供你玩。更新:它还有一个包含所有可能情况的功能草案。
因为您目前的核心问题似乎是如何计算 d
距离。它来自一个谈话,其中对于 skewX
的情况,倾斜操作本质上被描述为 (x, y) ↦ (x + d, y)。如果您查看 the SVG spec,您会发现一个矩阵,它基本上表示 skewX(a)
的确切公式是 (x, y) ↦ (x + cos(a) y, y)
。同样,对于 skewY(a)
,您有 (x, y) ↦ (x, y + cos(a) x)
。所以我会说第一种情况是 d := cos(a) * y
,第二种情况是 d := cos(a) * x
。
不过,问题标题向我提出了一个不同的问题。该问题可以表述如下:给定 transform="skewX(c) translate(a, b)"
一些数字 a
、b
和 c
,找到 d
和 e
,使得我刚刚给出的转换与 translate(d, e) skewX(c)
相同。或者换句话说:如果我想将转换移动到 skewX
.
的外部,我该如何更改 transform
的条目
要找到这些数字,请查看相应的矩阵乘积,如 defined in the spec:
⎡1 tan(c) 0⎤ ⎡1 0 a⎤ ⎡1 tan(c) a + tan(c) b⎤ ⎡1 0 a + tan(c) b⎤ ⎡1 tan(c) 0⎤
⎢0 1 0⎥∙⎢0 1 b⎥ = ⎢0 1 b ⎥ = ⎢0 1 b ⎥∙⎢0 1 0⎥
⎣0 0 1⎦ ⎣0 0 1⎦ ⎣0 0 1 ⎦ ⎣0 0 1 ⎦ ⎣0 0 1⎦
所以你会有 d = a + tan(c) * b
和 e = b
。您只需将倾斜变换应用于平移向量。换句话说:
skewX(c) translate(a, b) = translate(a + tan(c) * b, b) skewX(c)
你可以对y
做类似的计算得到:
skewY(c) translate(a, b) = translate(a, b + tan(c) * a) skewY(c)
如果你同时拥有skewX
和skewY
,你可以一次将translate
移出一步,这样在每一步你只需要处理一个单一倾斜方向。如果您想要相反的方向(即将 translate
移近倾斜内部),请在这些公式中使用 - tan(c)
而不是 + tan(c)
。
您编辑的问题及其包含的示例更清楚地表明您 真正 之后是将 CSS3 style="transform: …"
转换为等效的 SVG transform="…"
转换。特别是允许 CSS3 transform-origin: 50% 50% 0px
将变换中心置于对象中心的方式,而不是 SVG 坐标系的原点。
下面的代码片段演示了实现此目的的两种方法。一个相当简单:首先将对象的中心(您已经在问题的片段中计算出)平移到原点,然后执行所有转换,然后将该点平移回其原始坐标。那是中心的物体,它本质上有
transform="translate(256,256)
translate(20, 20)
rotate(45) skewX(20) skewY(-20) scale(0.6)
translate(-256,-256)"
但是在你的问题中你写道你想“避免使用链式 translate
”,上面使用了(在某种意义上)。为了避免这种情况,您可以将所有翻译步骤合并为一个。下面的代码就是这样做的,将翻译步骤移到外面,即移到序列的开头。最终结果基本上是
transform="translate(211.325,73.165)
rotate(45) skewX(20) skewY(-20) scale(0.6)"
除了每个数字的实际结果都有更多数字。我个人认为第一种方法更简单、更简洁,但第二种方法可能更接近您的想法。
一个特别的好处是代码按照转换描述中给出的顺序迭代基本转换,这样用户就可以自由地按照他们喜欢的任何顺序给出转换,并且翻译仍然可以得到适当收集。
var el1 = document.querySelectorAll('path')[0],
el2 = document.querySelectorAll('path')[1],
el2BB = el2.getBBox(), el2cx = el2BB.x + el2BB.width/2, el2cy = el2BB.y + el2BB.height/2,
el3 = document.querySelectorAll('path')[2],
transform = 'translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6)';
el1.style.transform = transform;
el1.style.transformOrigin = '50% 50% 0px';
transform = 'translate('+el2cx+','+el2cy+') ' + transform.replace(/deg/g,'').replace(/px/g,'')+' translate('+(-el2cx)+','+(-el2cy)+')';
el2.setAttribute('transform', transform);
el3.setAttribute('transform', combineTranslates(transform));
function combineTranslates(transform) {
var ts = [], // will contain list of elementary transformations
r = /\s*([A-Za-z0-9]+\s*\([\-0-9.,\s]*\))/g,
match,
pos = 0, // used during tokenization
deg = Math.PI/180.0,
x = 0, y = 0, // translation gets accumulated here
tmp;
// Tokenize transform into individual elementary transformations
while (match = r.exec(transform)) {
if (match.index !== pos) throw Error('Invalid transform: ' + transform);
pos += match[0].length;
ts.push(match[1]);
}
// TODO: check that only whitespace remains after matches
//console.log(ts);
// Iterate over transformations from inside to outside
for (var i = ts.length - 1; i >= 0; --i) {
match = /([A-Za-z0-9]+)\s*\(([\-0-9.,\s]*)\)/.exec(ts[i]);
var op = match[1],
args = match[2].replace(/\s+/g, '').split(',').map(Number);
//console.log(op, args);
switch (op) {
// Apply given transformation to (x,y) vector
case 'translate':
x += args[0];
y += args[1];
ts.splice(i, 1); // Drop translate from ts array
break;
case 'rotate':
var angle = args[0]*deg,
cos = Math.cos(angle),
sin = Math.sin(angle);
tmp = cos*x - sin*y;
y = sin*x + cos*y;
x = tmp;
break;
case 'scale':
x *= args[0];
y *= (args.length === 1 ? args[0] : args[1]);
break;
case 'skewX':
x += y*Math.tan(args[0]*deg);
break;
case 'skewY':
y += x*Math.tan(args[0]*deg);
break;
default:
throw Error('Unknown transform ' + op)
}
}
ts.unshift('translate('+x+','+y+')'); // add as first element
//console.log('From '+transform+'\n to '+ts.join(' '));
return ts.join(' ');
};
svg { overflow: visible; width:30%; border: 1px solid #eee; }
<svg id="svgMixedCSS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="green" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z" ></path>
</svg>
<svg id="svgMixedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="indigo" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
<svg id="svgCombinedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="blue" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
CONTEXT: @AnaTudor (@daredevil) is talking about 当 skewX
或 [=15= 时 SVG 的 d
移动距离] 被使用,所以我想知道是否有任何方法可以计算该距离以补偿平移,并避免使用链式 translate
.
测试用例:在下面的代码片段中,我们不会使用 SVG 特定链接 and/or 嵌套,我们只使用相同的变换值,但按照特定的顺序变换函数;
- 左侧绿色矩形 通过
style
:translate
、rotate
、skewX
、skewY
、scale
; - 右橄榄矩形 通过
transform
属性:translate
、scale
、rotate
、skewX
、 `偏斜。
现在,如您所见,两个矩形的位置不同,如果您单击按钮,第二个矩形会更接近我们的预期,但仍然需要针对所有情况进行更多计算。
问题:我们如何改变fixOrigin
函数来调整所有可能的变换函数组合的翻译,以一种看起来与[=63相同的方式=]变换?
var el1 = document.querySelectorAll('path')[0],
el2 = document.querySelectorAll('path')[1],
el2BB = el2.getBBox(), el2cx = el2BB.x + el2BB.width/2, el2cy = el2BB.y + el2BB.height/2,
btn = document.querySelectorAll('button')[0], btn1 = document.querySelectorAll('button')[1],
x = 20, y = 20, scale = 0.6, rotate = 45, skewX = 20, skewY = -20;
el1.style.transform = 'translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6)';
el1.style.transformOrigin = '50% 50% 0px';
el2.setAttribute('transform', 'translate('+x+','+y+') scale('+scale+') rotate('+rotate+' '+el2cx+','+el2cy+') skewX('+skewX+') skewY('+skewY+')');
function fixOrigin(){
x += (1-scale) * el2BB.width/2;
y += (1-scale) * el2BB.height/2;
el2.setAttribute('transform', 'translate('+x+','+y+') scale('+scale+') rotate('+rotate+' '+el2cx+','+el2cy+') skewX('+skewX+') skewY('+skewY+')');
}
btn.addEventListener('click',fixOrigin,false);
function fixEverything() {
// scale binds all transform functions together
if ( !!scale ) {
//most important make sure we have translation values
//!!(x) && (x=0); !!(y) && (y=0);
// first adjust translate based on scale value
x += (1-scale) * el2BB.width/2;
y += (1-scale) * el2BB.height/2;
//now we also adjust the rotation transform origin based on SKEWS
if (!!rotate) {
// el2cx += .... el2cy += ...
}
//almost there, now we adjust the translation based on SKEWS
// x += ... y += ...
// last case, when SKEWS are used alone
} else if ( !scale && !rotate ) {
// adjust translation here
// x += ... y += ...
}
el2.setAttribute('transform', 'translate(' + x + ',' + y + ') scale(' + scale + ') rotate(' + rotate + ' ' + el2cx + ',' + el2cy + ') skewX(' + skewX + ') skewY(' + skewY + ')');
}
btn1.addEventListener('click', fixEverything, false);
/* desired transform
transform-origin: 50% 50% 0px;
transform: translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6);
*/
svg {
overflow: visible; width:30%;
border: 1px solid #eee; /* some sort of ruler */
}
<button>Fix Transform Origin</button><button>Fix All</button><br>
<p>Click to change the `transform` attribute</p>
<svg id="svgMixedCSS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="green" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z" ></path>
</svg>
<svg id="svgMixedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="indigo" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
我制作了一个超酷的片段供你玩。更新:它还有一个包含所有可能情况的功能草案。
因为您目前的核心问题似乎是如何计算 d
距离。它来自一个谈话,其中对于 skewX
的情况,倾斜操作本质上被描述为 (x, y) ↦ (x + d, y)。如果您查看 the SVG spec,您会发现一个矩阵,它基本上表示 skewX(a)
的确切公式是 (x, y) ↦ (x + cos(a) y, y)
。同样,对于 skewY(a)
,您有 (x, y) ↦ (x, y + cos(a) x)
。所以我会说第一种情况是 d := cos(a) * y
,第二种情况是 d := cos(a) * x
。
不过,问题标题向我提出了一个不同的问题。该问题可以表述如下:给定 transform="skewX(c) translate(a, b)"
一些数字 a
、b
和 c
,找到 d
和 e
,使得我刚刚给出的转换与 translate(d, e) skewX(c)
相同。或者换句话说:如果我想将转换移动到 skewX
.
transform
的条目
要找到这些数字,请查看相应的矩阵乘积,如 defined in the spec:
⎡1 tan(c) 0⎤ ⎡1 0 a⎤ ⎡1 tan(c) a + tan(c) b⎤ ⎡1 0 a + tan(c) b⎤ ⎡1 tan(c) 0⎤
⎢0 1 0⎥∙⎢0 1 b⎥ = ⎢0 1 b ⎥ = ⎢0 1 b ⎥∙⎢0 1 0⎥
⎣0 0 1⎦ ⎣0 0 1⎦ ⎣0 0 1 ⎦ ⎣0 0 1 ⎦ ⎣0 0 1⎦
所以你会有 d = a + tan(c) * b
和 e = b
。您只需将倾斜变换应用于平移向量。换句话说:
skewX(c) translate(a, b) = translate(a + tan(c) * b, b) skewX(c)
你可以对y
做类似的计算得到:
skewY(c) translate(a, b) = translate(a, b + tan(c) * a) skewY(c)
如果你同时拥有skewX
和skewY
,你可以一次将translate
移出一步,这样在每一步你只需要处理一个单一倾斜方向。如果您想要相反的方向(即将 translate
移近倾斜内部),请在这些公式中使用 - tan(c)
而不是 + tan(c)
。
您编辑的问题及其包含的示例更清楚地表明您 真正 之后是将 CSS3 style="transform: …"
转换为等效的 SVG transform="…"
转换。特别是允许 CSS3 transform-origin: 50% 50% 0px
将变换中心置于对象中心的方式,而不是 SVG 坐标系的原点。
下面的代码片段演示了实现此目的的两种方法。一个相当简单:首先将对象的中心(您已经在问题的片段中计算出)平移到原点,然后执行所有转换,然后将该点平移回其原始坐标。那是中心的物体,它本质上有
transform="translate(256,256)
translate(20, 20)
rotate(45) skewX(20) skewY(-20) scale(0.6)
translate(-256,-256)"
但是在你的问题中你写道你想“避免使用链式 translate
”,上面使用了(在某种意义上)。为了避免这种情况,您可以将所有翻译步骤合并为一个。下面的代码就是这样做的,将翻译步骤移到外面,即移到序列的开头。最终结果基本上是
transform="translate(211.325,73.165)
rotate(45) skewX(20) skewY(-20) scale(0.6)"
除了每个数字的实际结果都有更多数字。我个人认为第一种方法更简单、更简洁,但第二种方法可能更接近您的想法。
一个特别的好处是代码按照转换描述中给出的顺序迭代基本转换,这样用户就可以自由地按照他们喜欢的任何顺序给出转换,并且翻译仍然可以得到适当收集。
var el1 = document.querySelectorAll('path')[0],
el2 = document.querySelectorAll('path')[1],
el2BB = el2.getBBox(), el2cx = el2BB.x + el2BB.width/2, el2cy = el2BB.y + el2BB.height/2,
el3 = document.querySelectorAll('path')[2],
transform = 'translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6)';
el1.style.transform = transform;
el1.style.transformOrigin = '50% 50% 0px';
transform = 'translate('+el2cx+','+el2cy+') ' + transform.replace(/deg/g,'').replace(/px/g,'')+' translate('+(-el2cx)+','+(-el2cy)+')';
el2.setAttribute('transform', transform);
el3.setAttribute('transform', combineTranslates(transform));
function combineTranslates(transform) {
var ts = [], // will contain list of elementary transformations
r = /\s*([A-Za-z0-9]+\s*\([\-0-9.,\s]*\))/g,
match,
pos = 0, // used during tokenization
deg = Math.PI/180.0,
x = 0, y = 0, // translation gets accumulated here
tmp;
// Tokenize transform into individual elementary transformations
while (match = r.exec(transform)) {
if (match.index !== pos) throw Error('Invalid transform: ' + transform);
pos += match[0].length;
ts.push(match[1]);
}
// TODO: check that only whitespace remains after matches
//console.log(ts);
// Iterate over transformations from inside to outside
for (var i = ts.length - 1; i >= 0; --i) {
match = /([A-Za-z0-9]+)\s*\(([\-0-9.,\s]*)\)/.exec(ts[i]);
var op = match[1],
args = match[2].replace(/\s+/g, '').split(',').map(Number);
//console.log(op, args);
switch (op) {
// Apply given transformation to (x,y) vector
case 'translate':
x += args[0];
y += args[1];
ts.splice(i, 1); // Drop translate from ts array
break;
case 'rotate':
var angle = args[0]*deg,
cos = Math.cos(angle),
sin = Math.sin(angle);
tmp = cos*x - sin*y;
y = sin*x + cos*y;
x = tmp;
break;
case 'scale':
x *= args[0];
y *= (args.length === 1 ? args[0] : args[1]);
break;
case 'skewX':
x += y*Math.tan(args[0]*deg);
break;
case 'skewY':
y += x*Math.tan(args[0]*deg);
break;
default:
throw Error('Unknown transform ' + op)
}
}
ts.unshift('translate('+x+','+y+')'); // add as first element
//console.log('From '+transform+'\n to '+ts.join(' '));
return ts.join(' ');
};
svg { overflow: visible; width:30%; border: 1px solid #eee; }
<svg id="svgMixedCSS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="green" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z" ></path>
</svg>
<svg id="svgMixedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="indigo" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
<svg id="svgCombinedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="blue" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>