【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を制御したい妄想の話
https://qiita.com/flying_echidna/items/da7ecc721650fa9ab651


#Mohoにはアクションとスマートボーンという機能があってな…

キャラクターのモデルを１つを使って、
歩きのアクション、瞬きのアクション、首振りのアクション…といったものを各々作ることができる。
それだけならまだしも、スマートボーンという機能を使えば、
各々のアクションのフレームをマージしながら、アニメーションを新しく作ることができる。

つまり？どういうことだってばよ？
つまり、歩きながら、好きなタイミングで瞬きさせて、好きなタイミングで好きな量だけ首を振れる。

フツーにこういったアニメーションを作る際は、顔の角度によって瞬きを書き直さないといけなかったり、
歩く動きで体が揺れると、角度も配置もより複雑になるので「もう上半身は上下運動でいいや」になっちゃったりするんだけども…
各々アクションとして用意しておけば、スマートボーンで合成したいフレームを指定するだけで、いい感じに合成してくれる。
つおい。

細かいアクションを用意すればするほど、微調整ができるようになる。
まあその分制御が大変なんだけども…

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">つまりこう <a href="https://t.co/Ma3v3Dbtua">pic.twitter.com/Ma3v3Dbtua</a></p>&mdash; BUN (@BUN_information) <a href="https://twitter.com/BUN_information/status/1240274721079353344?ref_src=twsrc%5Etfw">March 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このマージの方法を真似したい願望が、ないわけがなかろう。


#連番データの圧縮したいよなさすがに…

とはいえ、全アクションの全フレーム分、パスの位置・塗りのデータを持っておくと、無駄が多くてしかたない。
特に、動いていないパスのデータも毎フレームデータとして持つとか、ゲロ重にもほどがある。

と、いうことで。
各SVGファイル（各フレームのSVG）で、パスに変化があるかどうかをチェックしてから、データとして保持するようにしてみた。

どうするのか。
ぶっちゃけ、そんなに小難しいことはしてない。
そもそもSVGはタグで構成されてることもあり、DOMとして扱えてしまう。
つまり、JavaScriptでHTMLのタグを参照するノリで、SVGの中身を参照できる。
今回はisEqualNode()を使ってみた。

Node.isEqualNode - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/Node/isEqualNode

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

430行目から。

```Javascript:path_svg_loader.js
    let actionGroup = [];
    Object.keys(this.groupNameToIDList).forEach(name=> {
      let base = baseDom.getElementById(name);
      groupsDOMArr.some(targetDom=> {
        if( !targetDom || !base || !base.isEqualNode(targetDom.getElementById(name)) ) {
          actionGroup.push(name);
          return true;
        }
      });
      base = null;
    });
```

基本となる最初のフレームのSVGが入ったbaseDomと、全フレームのSVGが入ったgroupsDOMArrで、
一応中身が存在するかしないかチェックした上で、全レイヤー分順番に比較してる。
内容が一致しなければ、何かしら変更があったということで、actionGroupに名前を登録。
このあと必要な情報をピックアップして、データとして格納処理を走らしていく。

とはいえ、SVG上のフォーマットが異なるだけで、データとして見れば同一のものもあったりするもんで…
x座標y座標、色、線幅、などなど…同一かどうか全部のプロパティ分チェックするのか？と。
今後プロパティ増やそうものならは、毎回チェック項目を増やすのか？と。

そんなのお断りだ。JSON.stringify()を使う。

JSON.stringify() - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

これを使えば、JavaScriptのオブジェクトをJSON形式の文字列に変換できる。
`{"name":"hoge","x":20,"y":100,"rotate":0}`
といった感じで、中身が数字だろうが、元々文字列だろうが、全部JSON形式の文字列にしてくれるわけだ。
`JSON.stringify(currentData) == JSON.stringify(targetData)`
こんな雑な感じで比較して、データとして格納する。


#アクションとフレームのデータ管理どうするよ？

データを格納するはいいものの、実際に表示する時にはデータをマージしなきゃ意味がない。
全アクションの全フレームを格納した状態で、マージの対象となるアクション・フレーム数のデータを引っ張ってくる機能も必要だ。
軽く考えただけで、ぶっちゃけめんどくさい。

どのデータであれ「このアクションの、このフレームの時の値」が欲しくなるのは共通なんだから、
指定したアクションの、指定したフレームのデータを格納・取得できるようなクラスを用意してしまえばいいのだ。
やることは、C++で言うところの「テンプレートクラス」っぽいことなんだけども、
JavaScriptは「この変数、最初Stringとして初期化してたけど、別にあとからBoolean入れてもいいよ」ってぐらいデータの型がゆるっゆるなので、
その仕様に甘えで、ActionContainerなるクラスを実装した。

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

前項のJSON.stringify()を使った比較も、このクラスでやってる。
初期化とデータ追加部分は雑にこんな感じ。
3行目から。

```Javascript:ActionContainer.js
  constructor(data, checkFunc) {
    this.data = data;
    this.checkFunc = checkFunc;
    this.hasAction = Array.isArray(data) && data.some(val=>Array.isArray(val) && val.some(checkFunc));
    this.result = this.hasAction? data[0][0] : data;
  };
  
  setData(data, actionID = 0, frame = 0) {
    if(this.hasAction) {
      this.addAction(data, actionID, frame);
      return;
    }
    this.data = data;
    this.hasAction = Array.isArray(data) && data.some(val=>Array.isArray(val) && val.some(this.checkFunc));
    this.result = this.hasAction? data[0][0] : data;
  };
  
  addAction(data, actionID, frame) {
    if(!this.hasAction) {
      if( JSON.stringify(this.data) == JSON.stringify(data) ) return;
      
      // init action data
      this.data = [[this.data]];
      this.hasAction = true;
    }
    if(typeof this.data[actionID] === "undefined") {
      this.data[actionID] = [this.data[0][0].concat()];
    }
    
    let isEmpty = true;
    for(let i = this.data[actionID].length - 1; i >= 0; --i) {
      if(typeof this.data[actionID][i] === "undefined") continue;
      if(JSON.stringify(data) == JSON.stringify(this.data[actionID][i])) break;
      this.data[actionID][frame] = data;
      isEmpty = false;
      break;
    }
    if(isEmpty) {
      this.data[actionID][frame] = undefined;
    }
  };
```

hasActionとかいうBooleanのメンバ変数があるんだけども。
「ベースとなるメインアクション以外にアクションがないなら、アクション別で格納しようとすること自体無駄でしょ？」
ということで、他にアクションを持ってるかどうかをデータとして持ってる。

もし他のアクションのデータも持ってた場合は二次元配列化して、
`this.data[actionID][frame]`
でアクションIDとフレーム数を指定すれば、データが引っ張って来れるようにしてる。
JavaScriptの配列の仕様上、該当のデータが存在してない（何も格納されてない）ときはundefinedが返ってくるし、
配列の長さが足りないインデックスを指定して代入しても、間を飛ばしてデータが格納できる。
雑ぅ！！

checkFuncはデータチェック用のメソッドを初期化時に指定する。
チェックでtrueが返ってきたら1フレーム分のデータ。
そうでなければdata[actionID][frame]の形式のデータと想定して扱ってる。
二次元配列かどうかのチェックはdata[0][0]（メインアクションの最初のフレーム）のデータが存在してるかどうかでしか見てない。
わぁお雑ぅ！！

あとはデータの取得も楽にできるようにメソッドを用意しておく。
50行目から。

```Javascript:ActionContainer.js
  getData(actionID = 0, frame = 0) {
    if(!this.hasAction) return this.data;
    return this.data[actionID][Math.min(frame, this.data[actionID].length-1)];
  };
  
  getAvailableData(actionID = 0, frame = 0) {
    if(!this.hasAction) {
      return this.data;
    }
    if(!this.hasActionID(actionID)) {
      return this.data[0][0];
    }
    
    for(let targetFrame = frame; targetFrame >= 0; --targetFrame) {
      let targetData = this.getData(actionID, targetFrame);
      if(typeof targetData !== "undefined") return targetData;
    }
    return undefined;
  };
```

指定されたアクション、指定されたフレームにデータが存在しないこともあり得るので、
指定アクションがなければメインアクションの最初のフレームのデータを返す。
指定アクションの指定フレームから0フレーム目にかけてデータを探して、一番最初に見つかったデータを返す。
見つからなかったら…データがねぇよ、と。


#データのマージしなきゃ

とにはかくにも、用意したActionContainerを使ってデータを格納する。

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

6行目から。

```Javascript:PathObj.js
    this.pathDiffList = new ActionContainer(pathDiffList, val=>Array.isArray(val) && val.some(v=>Array.isArray(v)));  // diff pos data array
    this.fillStyle = new ActionContainer(fillStyle, val=>typeof val === "string");  // fillColor ( context2D.fillStyle )
    this.lineWidth = new ActionContainer(lineWidth, val=>Number.isFinite(val));  // strokeWidth ( context2D.lineWidth )
    this.strokeStyle = new ActionContainer(strokeStyle, val=>typeof val === "string");  // strokeColor ( context2D.strokeStyle )
```

addAction()を使ってデータを追加する。
追加しようとしているデータが、どのアクションのものに該当して、どのフレームに該当するのか、指定して渡す。
39行目から。

```Javascript:PathObj.js
    this.pathDiffList.addAction(pathDiffList, actionID, frame);
    this.fillStyle.addAction(fillStyle, actionID, frame);
    this.lineWidth.addAction(lineWidth, actionID, frame);
    this.strokeStyle.addAction(strokeStyle, actionID, frame);
```

さて。実際に表示するとき、どうマージするか。
PathObj側は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()ってのを使ってて、他はupdate()を使ってる。
途中のmatrixがどうこうってのは…次の記事で書くから待って…


##変化があったら引っ張ってくるだけ方式マージ

先にActionContainerクラスのupdate()内でやってることの説明。
感覚でいうと、バージョン管理ソフトで変更があった行を引っ張ってくるような感じのマージ。
変化した部分として検出できた値をそのまま参照する方式。

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

70行目から。

```Javascript:ActionContainer.js
  update(pathContainer, actionID = 0, frame = 0) {
    if(!this.hasAction) return;
    
    if(!this.hasActionID(actionID)) {
      actionID = 0;
      frame = 0;
    }
    
    let output =(action, val)=> {
      if(!PathCtr.isOutputDebugPrint) return;
      if(this.result == val) return;
      PathCtr.debugPrint(action.name, action.pastFrame, action.currentFrame, val);
    };
    let data = null;
    
    this.data.forEach((actionDataList, targetActionID)=> {
      let action = pathContainer.actionList[targetActionID];
      let pastFrame = action.pastFrame;
      let currentFrame = action.currentFrame;
      
      if(pastFrame == currentFrame) return;
      
      if(pastFrame <= currentFrame) {
        for(let targetFrame = Math.min(currentFrame, actionDataList.length-1); targetFrame >= pastFrame; --targetFrame) {
          let targetData = actionDataList[targetFrame];
          if(typeof targetData === "undefined") continue;
          data = targetData;
          output(action, data);
          break;
        }
      } else {
        for(let targetFrame = Math.min(pastFrame, actionDataList.length-1); targetFrame >= currentFrame; --targetFrame) {
          let targetData = actionDataList[targetFrame];
          if(typeof targetData === "undefined") continue;
          
          for(let targetFrame = Math.min(currentFrame, actionDataList.length-1); targetFrame >= 0; --targetFrame) {
            let targetData = actionDataList[targetFrame];
            if(typeof targetData === "undefined") continue;
            data = targetData;
            output(action, data);
            break;
          }
          break;
        }
      }
    });
    
    if(!!data && this.result != data) {
      this.result = data;
    }
  };
};
```

あ、途中のoutput()は、ただのデバッグ表示用なので無視でオナシャス。

各アクションで、現在再生しようとしているフレームと、直前に再生していたフレームのデータを持ってる。
まったく同じフレームなら変更なし。
現在のフレームが格納されている全フレームよりも大きい値だった場合は、最終フレーム数を参照する。

直前のフレームより現在のフレームの方が値が大きい場合は、
現在のフレームから直前のフレームにかけてデータを走査（逆走）して、
一番最初に見つかったデータ（現在のフレームに一番近い値）を結果として扱う。

直前のフレームより現在のフレームの方が値が小さい場合（逆再生）は、
直前のフレームから現在のフレームの間に値の変化があるときのみ、
現在のフレームから0フレーム目かけてデータを走査（逆走）して、
一番最初に見つかったデータ（現在のフレームに一番近い値）を結果として扱う。

各アクションを参照して、値に変化があったときだけ、その値を引っ張ってくる、といった感じ。
しかもアクションのIDが後半のデータが優先されてる。


##全部参照して計算する方式マージ

どちらかというと、難題はこっち。
PathObjクラスのgetMergePathDataList()の方。
パスのマージはさすがに先ほどのマージ方法では、欲しい値にならない。

とある点のx座標の元の値が10だとして、
アクションAの現在フレームではx座標20（元の値から+10）、
アクションBの現在フレームではx座標0（元の値から-10）、
になっているとしよう。
先ほどのマージの方法であれば…
順番が後になっているアクションBの内容が優先されてx座標0になる。
…ちゃうねん。
10 +10 -10 = 10
にしたいねん。

つまり？
元の値を基準とした座標の『差分』を、全アクション分合計したい。
てかもう、座標のデータは差分だけでいいんじゃね？

ということで、パスのデータだけは、最初に差分を保存するようにしていたりする。
あと、Mohoではどのアクションにせよ点の数は同一なので、そのチェックをしてる。
SVG出力した際に、同じ位置にある点のデータが吹き飛ぶという問題が実はあったりするのよね…

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

パスのアクションのデータを保存する処理。
14行目から。

```Javascript:PathObj.js
  addAction(pathDataList, fillStyle, lineWidth, strokeStyle, frame, actionID) {
    if(!pathDataList) {
      pathDataList = this.defPathList.concat();
    } else if(this.defPathList.length != pathDataList.length) {
      console.error("The number of paths does not match.");
      console.log(this.defPathList);
      console.log(pathDataList);
      pathDataList = this.defPathList.concat();
    }
    
    let pathDiffList = [];
    this.defPathList.forEach((d, i)=>{
      if(d.type != pathDataList[i].type) {
        console.error("type does not match.");
        console.log(this.defPathList);
        console.log(pathDataList);
        return;
      }
      if(!d.pos) return;
      pathDiffList[i] = [];
      d.pos.forEach((val, j)=>{
        pathDiffList[i].push(pathDataList[i].pos[j] - val);
      });
    });
    
    this.pathDiffList.addAction(pathDiffList, actionID, frame);
    this.fillStyle.addAction(fillStyle, actionID, frame);
    this.lineWidth.addAction(lineWidth, actionID, frame);
    this.strokeStyle.addAction(strokeStyle, actionID, frame);
  };
```

defPathListが元となるメインアクションの最初のフレームの段階でのパスの座標のリスト。
この値でアクションとして登録する予定のpathDiffListの座標を差分に変換してる。

ここまで下準備をした上で…
PathObjクラスのgetMergePathDataList()で全アクションをマージする。
75行目から。

```Javascript:PathObj.js
  getMergePathDataList(pathContainer, frame = 0, actionID = 0) {
    if(!this.pathDiffList.hasAction) {
      return this.makeData(this.pathDiffList.getData());
    }
    
    if(!this.pathDiffList.hasActionID(actionID)) {
      actionID = 0;
      frame = 0;
    }
    
    let pathDataList = this.makeData(this.pathDiffList.getAvailableData(actionID, frame));
    if(pathContainer.actionList.length == 1) {
      return pathDataList;
    }
    
    pathContainer.actionList.forEach(action=>{
      if(actionID == action.id) return;
      if(!this.pathDiffList.hasActionID(action.id)) return;
      this.pathDiffList.getAvailableData(action.id, action.currentFrame).forEach((list, i)=>{
        if(!list) return;
        list.forEach((val, j)=>{
          if(!val) return;
          pathDataList[i].pos[j] += val;
        });
      });
    });
    return pathDataList;
  };
```

例外チェックしながら、ひたすらループで足し算するだけ。

次の記事で「レイヤーの変形したいんだが？」な話をすることにする。

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