■実物
【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
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
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行目から。
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行目から。
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