LoginSignup
0
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-31

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

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

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

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

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

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

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

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

【SVG制御妄想7】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

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

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

これを各レイヤーに該当するところで使う、と。
今回作っているブツでは、複数のパスを1つのレイヤーに格納しているだけでなく、
レイヤーの中にレイヤーを入れ子で配置できる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行目から。

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行目から。

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制御妄想5】ボーンぐりぐり
https://qiita.com/flying_echidna/items/a34648da8a650fe34824

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0