【SVG制御妄想４】変形させたいよなぁ？

■実物
【GitHub】SourceOf0-HTML/path_control: SVGを制御したい願望
https://github.com/SourceOf0-HTML/path_control

【GitHub Pages】ベクターデータをいじり倒したい気持ち
https://sourceof0-html.github.io/path_control/


■記事一覧
【SVG制御妄想１】SVG解析しないと始まらない
https://qiita.com/flying_echidna/items/5a628db0d652d1558208

【SVG制御妄想２】Mohoから出力したSVGのマスクがバグる
https://qiita.com/flying_echidna/items/3930caf04626deec7bfb

【SVG制御妄想３】連番データをどげんかせんと
https://qiita.com/flying_echidna/items/ded3f3590c3d67fadb86

【SVG制御妄想４】変形させたいよなぁ？
https://qiita.com/flying_echidna/items/188634f35a05bbde9a51

【SVG制御妄想５】ボーンぐりぐり
https://qiita.com/flying_echidna/items/a34648da8a650fe34824

【SVG制御妄想６】助けてマルチスレッド
https://qiita.com/flying_echidna/items/80b101c1a1eedb534137

【SVG制御妄想７】SVGの限界
https://qiita.com/flying_echidna/items/2f53a461c5e6c05109df


■過去記事
【2019-03-06】「SVGでアニメーションさせたいんじゃ」の詳細報告
https://qiita.com/flying_echidna/items/ff3a061f4e348e62cca0

【2020-02-13】Mohoから出力したSVGを制御したい妄想の話 - Qiita
https://qiita.com/flying_echidna/items/da7ecc721650fa9ab651


#いそのーアフィン変換しようぜー

「アフィン変換」って結局のところなんやねん。

雑ぅーに言えば、画像を移動・拡縮・回転させるときに使う数列。
レイヤーを移動させたいだが？拡大したいんだが？回転したいんだが？
そういうときに内部的に使われてる数列。

もうちょい厳密に言えば…

元の画像の座標(x1, y1)を、この行列で変換して、新しい画像の座標(x2, y2)にする。
この新しい画像が、元の画像を移動・拡縮・回転させたもの、っていう感じ。
行列の各々の数値をいじることで、画像をどう変形させるか変更できる。

今回ベクタ形式（パスの座標などなど）を自力で制御するってことで、
ここらへんも自作してアレコレいじり倒したいわけであります。

わかりやすいサイト様

アフィン変換　画像処理ソリューション
https://imagingsolution.blog.fc2.com/blog-entry-284.html

で、今回実装するにあたり参考にさせて頂いたソースコード

【GitHub】transformation-matrix-js/matrix.js at master ・ leeoniya/transformation-matrix-js
https://github.com/leeoniya/transformation-matrix-js/blob/master/src/matrix.js

で、実際に書いたソースコード

【GitHub】path_control/Matrix.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/Matrix.js

```Javascript:Matrix.js
class Matrix {
  constructor() {
    this.a = 1;  // scale x
    this.b = 0;  // skew y
    this.c = 0;  // skew x
    this.d = 1;  // scale y
    this.e = 0;  // translate x
    this.f = 0;  // translate y
  };
  
  reset() {
    this.a = this.d = 1;
    this.b = this.c = this.e = this.f = 0;
    return this;
  };
  
  /**
   * @param {Array} point
   * @param {Integer} index
   */
  applyToPoint(point, index = 0) {
    let x = point[index];
    let y = point[index+1];
    point[index] = x * this.a + y * this.c + this.e;
    point[index+1] = x * this.b + y * this.d + this.f;
  };
  
  /**
   * @param {Array} points
   * @param {Integer} index
   */
  applyToArray(points, index = 0) {
    let pointsNum = points.length;
    for(let i = index; i < pointsNum; i += 2) {
      this.applyToPoint(points, i);
    }
  };
  
  /**
   * @param {Matrix} m
   * @return {Matrix}
   */
  setMatrix(m) {
    this.a = m.a;
    this.b = m.b;
    this.c = m.c;
    this.d = m.d;
    this.e = m.e;
    this.f = m.f;
    return this;
  };
  
  /**
   * @param {Matrix} m2 - Matrix
   * @param {Number} t - interpolation [0.0, 1.0]
   * @return {Matrix} - new Matrix
   */
  interpolate(m2, t) {
    let m = new Matrix();
    m.a = this.a + (m2.a - this.a) * t;
    m.b = this.b + (m2.b - this.b) * t;
    m.c = this.c + (m2.c - this.c) * t;
    m.d = this.d + (m2.d - this.d) * t;
    m.e = this.e + (m2.e - this.e) * t;
    m.f = this.f + (m2.f - this.f) * t;
    return m;
  };
  
  /**
   * @param {Number} t
   * @return {Matrix} - new Matrix
   */
  mult(t) {
    let m = new Matrix();
    m.a = this.a * t;
    m.b = this.b * t;
    m.c = this.c * t;
    m.d = this.d * t;
    m.e = this.e * t;
    m.f = this.f * t;
    return m;
  };
  
  /**
   * @param {Number} t
   * @param {Number} x
   * @param {Number} y
   * @param {Array} point
   * @param {Integer} index
   */
  multAndAddPoint(t, x, y, point, index) {
    point[index] += (x * this.a + y * this.c + this.e) * t;
    point[index+1] += (x * this.b + y * this.d + this.f) * t;
  };
  
  /**
   * @param {Number} a - scale x
   * @param {Number} b - skew y
   * @param {Number} c - skew x
   * @param {Number} d - scale y
   * @param {Number} e - translate x
   * @param {Number} f - translate y
   * @return {Matrix}
   */
  setTransform(a, b, c, d, e, f) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
    this.e = e;
    this.f = f;
    return this;
  };
  
  /**
   * @param {Number} a2 - scale x
   * @param {Number} b2 - skew y
   * @param {Number} c2 - skew x
   * @param {Number} d2 - scale y
   * @param {Number} e2 - translate x
   * @param {Number} f2 - translate y
   * @return {Matrix}
   */
  transform(a2, b2, c2, d2, e2, f2) {
    let a1 = this.a,
        b1 = this.b,
        c1 = this.c,
        d1 = this.d,
        e1 = this.e,
        f1 = this.f;
    this.a = a1 * a2 + c1 * b2;
    this.b = b1 * a2 + d1 * b2;
    this.c = a1 * c2 + c1 * d2;
    this.d = b1 * c2 + d1 * d2;
    this.e = a1 * e2 + c1 * f2 + e1;
    this.f = b1 * e2 + d1 * f2 + f1;
    return this;
  };
  
  rotate(angle) {
    let cos = Math.cos(angle);
    let sin = Math.sin(angle);
    return this.transform(cos, sin, -sin, cos, 0, 0);
  };
  
  transformFromMatrix(m2) { return this.transform(m2.a, m2.b, m2.c, m2.d, m2.e, m2.f) };
  scale(sx, sy = sx) { return this.transform(sx, 0, 0, sy, 0, 0) };
  scaleX(sx) { return this.transform(sx, 0, 0, 1, 0, 0) };
  scaleY(sy) { return this.transform(1, 0, 0, sy, 0, 0) };
  
  skew(sx, sy) { return this.transform(1, sy, sx, 1, 0, 0) };
  skewX(sx) { return this.transform(1, 0, sx, 1, 0, 0) };
  skewY(sy) { return this.transform(1, sy, 0, 1, 0, 0) };
  
  translate(tx, ty) { return this.transform(1, 0, 0, 1, tx, ty) };
  translateX(tx) { return this.transform(1, 0, 0, 1, tx, 0) };
  translateY(ty) { return this.transform(1, 0, 0, 1, 0, ty) };
};
```


さて。
正直に言うと、このままじゃ使いづらい。
行列にしてしまうと、回転の情報と移動の情報がゴッチャになるし、
移動させたい座標と、回転量を別々で指定したいし、
処理の途中で「今どれぐらい回転させてるっけ？」と確認もしたい。

と、いうことでSpriteというクラスを作った。
やったことは単純。

移動量：x, y
拡縮比率：scaleX, scaleY
回転量：rotation
拡縮や回転の中心座標：anchorX, anchorY

これをデータとして持っていて、
この情報を元にMatrixを生成するクラス。
x = 1
とすれば1だけ移動できて、
scaleX = 2
とすればanchorXを中心として、x座標方向に2倍拡大できて、
rotation = Math.PI
とすれば、180度（弧度法なので$ \pi $）回転できる。
そんな感じ。

実際に書いたソースコード

【GitHub】path_control/Sprite.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/Sprite.js

```Javascript:Sprite.js
class Sprite {
  constructor() {
    this.m = new Matrix();
    this.x = 0;
    this.y = 0;
    this.anchorX = 0;
    this.anchorY = 0;
    this.scaleX = 1;
    this.scaleY = 1;
    this.rotation = 0;
  };
  
  reset() {
    this.x = 0;
    this.y = 0;
    this.anchorX = 0;
    this.anchorY = 0;
    this.scaleX = 1;
    this.scaleY = 1;
    this.rotation = 0;
  };
  
  /**
   * @return {Sprite}
   */
  clone() {
    let sprite = new Sprite();
    sprite.x = this.x;
    sprite.y = this.y;
    sprite.anchorX = this.anchorX;
    sprite.anchorY = this.anchorY;
    sprite.scaleX = this.scaleX;
    sprite.scaleY = this.scaleY;
    sprite.rotation = this.rotation;
    return sprite;
  };
  
  /**
   * @param {Sprite} s
   * @return {Sprite}
   */
  setSprite(sprite) {
    this.x = sprite.x;
    this.y = sprite.y;
    this.anchorX = sprite.anchorX;
    this.anchorY = sprite.anchorY;
    this.scaleX = sprite.scaleX;
    this.scaleY = sprite.scaleY;
    this.rotation = sprite.rotation;
    return this;
  };
  
  /**
   * @param {Sprite} s
   * @return {Sprite}
   */
  addSprite(sprite) {
    this.x +=sprite.x;
    this.y += sprite.y;
    this.anchorX += sprite.anchorX;
    this.anchorY += sprite.anchorY;
    this.scaleX *= sprite.scaleX;
    this.scaleY *= sprite.scaleY;
    this.rotation = sprite.rotation;
    return this;
  };
  
  /**
   * @param {Sprite} s
   * @return {Sprite} - new Sprite
   */
  compSprite(sprite) {
    let ret = new Sprite();
    ret.x = this.x + sprite.x;
    ret.y = this.y + sprite.y;
    ret.anchorX = this.anchorX + sprite.anchorX;
    ret.anchorY = this.anchorY + sprite.anchorY;
    ret.scaleX = this.scaleX * sprite.scaleX;
    ret.scaleY = this.scaleY * sprite.scaleY;
    ret.rotation = this.rotation + sprite.rotation;
    return ret;
  };
  
  /**
   * @param {Number} offsetX
   * @param {Number} offsetY
   * @return {Matrix}
   */
  getMatrix(offsetX = 0, offsetY = 0) {
    return this.m.reset().translate(this.x + offsetX, this.y + offsetY).rotate(this.rotation).scale(this.scaleX, this.scaleY).translate(- this.anchorX - offsetX, - this.anchorY -  + offsetY);
  };
};
```

これを各レイヤーに該当するところで使う、と。
今回作っているブツでは、複数のパスを１つのレイヤーに格納しているだけでなく、
レイヤーの中にレイヤーを入れ子で配置できるGroupObjクラスを用意した。
GroupObjクラス自体はSpriteクラスを継承させていて、入れ子になっているレイヤーに対しては、親レイヤーの変形を子レイヤーに反映するようにしてる。

【GitHub】path_control/GroupObj.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/GroupObj.js

33行目から。

```Javascript:GroupObj.js
  update(pathContainer, sprite, flexiIDs = []) {
    let actionID = pathContainer.currentActionID;
    let frame = pathContainer.actionList[actionID].currentFrame;
    let groupSprite = sprite.compSprite(this);
    let flexi = flexiIDs.concat(this.flexi);
    let groupMatrix = groupSprite.getMatrix();
    
    this.paths.forEach(path=> {
      path.update(frame, actionID, pathContainer, groupMatrix);
    });
    
    this.childGroups.result.forEach(childGroup=> {
      pathContainer.groups[childGroup].update(pathContainer, groupSprite, flexi);
    });
    
    if(flexi.length <= 0) return;
    this.paths.forEach(path=>path.calcFlexi(pathContainer, flexi));
  };
```

flexiについては次の記事で…
update()の第2引数で渡されたspriteと、今のレイヤーのspriteの情報をcompSprite()で合成してる。
で、合成結果をこのレイヤーの入れ子になっているレイヤーとパスに渡してる。

【GitHub】path_control/PathObj.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/PathObj.js

158行目から。

```Javascript:PathObj.js
  update(frame, actionID, pathContainer, matrix) {
    let pathDataList = this.getMergePathDataList(pathContainer, frame, actionID);
    
    pathDataList.forEach(d=>{
      if(!!d.pos) matrix.applyToArray(d.pos);
    });
    
    this.resultPathList = pathDataList;
    
    this.fillStyle.update(pathContainer, actionID, frame);
    this.lineWidth.update(pathContainer, actionID, frame);
    this.strokeStyle.update(pathContainer, actionID, frame);
  };
```

前回の記事で説明したgetMergePathDataList()で取得した座標をapplyToArray()で変形させてリザルトとして格納、と。
なのでresultPathListが現在のこのパスの座標で確定なので描画にそのまま使える…

と　で　も　思　っ　て　い　た　の　か　？

ちゃうねん。
もう一個やろうと思って、この段階ではまだやってないことがあるのよ。
ボーンによる制御。
その話は次の記事で…

次の記事：【SVG制御妄想５】ボーンぐりぐり
https://qiita.com/flying_echidna/items/a34648da8a650fe34824