如何在SVG上使用skewX和skewY时补偿平移

时间:2016-08-28 12:01:03

标签: javascript math svg transform skew

语境:@AnaTudor(@daredevil)is talking aboutdskewX被使用时,SVG的移动距离skewY,所以我想知道是否有任何方法可以计算该距离以补偿翻译,并避免使用链式translate

测试用例:在下面的snipet中,我们使用了SVG特定的链接和/或嵌套,我们只使用相同的变换值,但是按照特定的变换函数顺序;

  • LEFT绿色矩形通过styletranslaterotateskewXskewYscale;
  • 通过transform属性
  • 正确的橄榄色矩形translatescalerotateskewX,`skewY。

现在,正如您所看到的,两个矩形具有不同的定位,如果单击按钮,第二个矩形将更接近我们的预期,但仍需要为所有情况计算更多。

问题:我们如何更改fixOrigin函数以调整所有可能的转换函数组合的转换,其方式与CSS3转换相同?



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>
&#13;
&#13;
&#13;

我做了一个非常酷的snipet让你玩。更新:它还有一个功能草案,包含所有可能的情况。

2 个答案:

答案 0 :(得分:1)

由于您当前的核心问题似乎是如何计算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)"ab给出c,找到de,以便我刚给出的转换与translate(d, e) skewX(c)相同。或者换句话说:如果我想将变换移动到transform的外部,我如何更改skewX的条目。

要查找这些数字,请查看相应的矩阵产品,如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) * be = 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)

如果您同时使用skewXskewY,则可以一次将translate移出一步,这样您在每一步都只需处理一次歪斜方向。如果您想要相反的方向(即将translate移近歪斜的内侧),请在这些公式中使用- tan(c)而不是+ tan(c)

答案 1 :(得分:1)

您编辑的问题及其中包含的示例更清楚地表明您真正之后将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)"

除了每个这些数字的实际结果都有更多数字。我个人认为第一种方法更简单,更清洁,但第二种方法可能更接近您的想法。

一个特别的好处是,代码按照转换描述中给出的顺序迭代基本转换,以便用户可以按照自己喜欢的顺序自由地进行转换,并且仍然可以适当地收集转换

&#13;
&#13;
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(' ');
};
&#13;
svg { overflow: visible; width:30%; border: 1px solid #eee; }
&#13;
<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>
&#13;
&#13;
&#13;