この記事は「続・Mohoから出力したSVGを制御したい妄想の話」の一部です。
■実物
【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
#いろいろやりすぎて重すぎワロタ
何が一番重いって、ロードよね。
テスト用に使ってるSVGファイルの総数、1140枚。
これを一気にブラウザでロードしてるわけで。
だから仕方なしにSVGファイルをそのまま読み込んで使う処理とは別で、
一般公開用にバイナリ化して出力してるバージョンも用意したりもしてるわけで。
しかし、それだけじゃない。
実は、何の工夫もなくそのまま全部のSVGを読み込ませようとすると…
ブラウザがメモリ食いすぎてクラッシュする。
酷い。それはさすがに酷すぎる。
でも、クラッシュする原因がメモリの食いすぎだとしても、
うまいこと読み込みとメモリ開放をすれば、一気にメモリを食うこともないはず。
ということで、setTimeout()を使った時差読み込みをするようにした。
【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
442行目から。
groupsDOMArr.forEach((targetDom, frame)=> {
if(frame == 0) return;
if(frame % 10 == 0) console.log("add action : " + actionName + " - " + frame);
actionGroup.forEach((name, i)=> {
setTimeout(this.addActionGroup, 0, i, targetDom.getElementById(name), name, frame, actionName);
});
});
setTimeout(this.loadDOMEnd);
おかげでクラッシュはしなくなった。
…が。
余計に処理が遅くなった。そりゃそうだ。
時差読み込みするということは、一旦処理を終えて、もう一回走らせるようなもんで。
その間にJavaScriptのガベージコレクションが働いて、メモリを解放してくれていたとしても…
その分時間がかかるに決まってる。
実行までに5分かかるとか、勘弁ならん。
今時ブラウザのページの読み込みに5分とか、発狂じゃん。
おい!!
JavaScriptでマルチスレッドの実装はできんのか!!
#Web Workerなるものがあるぞ
WebWorker?なんそれ。
Web Worker の使用 - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers
なんとまあ、JavaScriptでマルチスレッドできるってよ。
ということで、せっかくだから使ってみる。
…んだけども。
WebWorkerは、元々別のjsファイルのある場所を指定して、読み込ませるものなんだとか。
大量のコードを1つにまとめてミニファイしてやろうと企んでたもんだから、ファイル分割は若干痛手。
しかもHTMLで読み込むファイルパスとは違って、WebWorkerを初期化したJavaScriptの場所からのファイルパスになる模様。
カレントディレクトリがHTML側と異なってくる。めんどくさっ!
おまけに扱えるオブジェクトに制限があるとか。
Web Workers が使用できる関数とクラス - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers
この中で一番痛いのは、WebWorkerからだとDOMがいじれないこと。
めんどくせぇな。
#別のjsファイルを読み込むとか嫌でござる
jsファイルをWebWorker用に分けたとしよう。
使うときは毎回複数あるjsファイルを一緒に置く必要が出てくる。
でも、どっちか欠けたら動かない。
事故る原因にもなるのに、ファイルをわけでどーすんだ。
だったらjsファイルの中でjsファイルを生成すればいいじゃない。
元々、実際にHTMLで読み込ませるjsファイルを一つにするために、バッチファイルでまとめるようにしてた。
このバッチファイル内で、WebWorkerにしたいjsファイルを文字列にしてしまって、Blobで読み込む、という荒業。
【GitHub】path_control/_comp.bat ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/_comp.bat
このバッチファイル、デバッグバージョンや、SVGダイレクト読み込みバージョンのjsファイルを別々で生成するようにしてるんだけども…
28行目から。
set BIN_FILE=%DIR_PATH%path_control.js
del %BIN_FILE%
type LICENSE.txt >> %BIN_FILE%
echo let path_control = ` >> %BIN_FILE%
type %CORE_SRC% >> %BIN_FILE%
echo ` >> %BIN_FILE%
type path_main.js >> %BIN_FILE%
真ん中の行で突然path_controlという変数を宣言してる、これがそう。
ファイルをそのまま全部文字列化した挙句、変数にぶち込んでる。
で、こいつを…
【GitHub】path_control/path_main.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/path_main.js
203行目から。
let blob = new Blob([path_control], {type: "text/javascript"});
let filePath = window.URL.createObjectURL(blob);
これでOK。
あとは「これはjsファイルですよ?何か?」と言わんがばかりに、
new Worker(filePath);
をすれば、WebWorkerが使える。
#で?ロードどうするよ?
問題はここ。
ロードはSVGをHTMLの中に展開することで行ってる。
でもそれはDOM操作なので、WebWorkerだけでは行えない。
じゃあDOM操作とデータ化をメインスレッドでして、時間のかかるファイルのロードをWebWorkerですればいいじゃない。
ということで…
【GitHub】path_control/path_load_svg_worker.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/path_load_svg_worker.js
let fileInfoList = null;
let fileIndex = 0;
let getFrameNum=i=>("00000".substr(0, 5 - i.toString().length) + i + ".svg");
addEventListener("message", function(e) {
let loadFile=()=> {
let fileInfo = fileInfoList[fileIndex++];
let kind = fileInfo[0];
let totalFrames = fileInfo[1];
let actionName = fileInfo[2];
let filePath = fileInfo[3];
let loadFrame = 1;
let request = new XMLHttpRequest();
let loadSVG = request.onreadystatechange = function(e) {
let target = e.target;
if(target.readyState != 4) return;
if((target.status != 200 && target.status != 0) || target.responseText == "") {
console.error("failed to read file: " + target.responseURL);
console.error(target.statusText);
return;
}
postMessage({
cmd: "new-svg",
actionName: actionName,
frame: loadFrame,
svg: target.responseText,
});
delete request;
if(loadFrame <= totalFrames) {
request = new XMLHttpRequest();
//console.log(filePath + getFrameNum(loadFrame));
request.open("GET", filePath + getFrameNum(loadFrame++), true);
request.onreadystatechange = loadSVG;
request.send();
return;
}
if(fileIndex < fileInfoList.length) {
postMessage({
cmd: "load-add",
kind: kind,
actionName: actionName,
totalFrames: totalFrames,
});
} else {
postMessage({
cmd: "load-complete",
kind: kind,
actionName: actionName,
totalFrames: totalFrames,
});
close();
}
};
//console.log(filePath + getFrameNum(loadFrame));
request.open("GET", filePath + getFrameNum(loadFrame++), true);
request.send();
};
let data = e.data;
switch (data.cmd) {
case "load":
fileIndex = 0;
fileInfoList = data.fileInfoList;
loadFile();
break;
case "load-next":
loadFile();
break;
default:
console.error("unknown command: " + data.cmd);
break;
};
}, false);
指定されたSVGのファイルを片っ端から読み込むだけのWebWorker用jsファイル。
postMessage()でメインスレッドに途中経過とメッセージ(読み込んだSVGの文字列等)を渡してる。
【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
587行目から。
this.loadWorker.addEventListener("message", function(e) {
let data = e.data;
switch(data.cmd) {
case "load-complete":
SVGLoader.loadFromDOM(data.kind, data.actionName, data.totalFrames);
setTimeout(SVGLoader.loadEnd);
break;
case "load-add":
SVGLoader.loadFromDOM(data.kind, data.actionName, data.totalFrames);
break;
case "new-svg":
if(data.frame % 10 == 0) console.log("load file: " + data.actionName + " - " + data.frame);
SVGLoader.addSvgDOM(data.svg);
break;
default:
console.error("unknown command: " + data.cmd);
break;
}
});
いいね。
#マルチスレッドできるなら描画処理分けたいよね
描画処理がメインスレッドで走ってると、何が嫌なのか。
仮に描画処理が重かったとしたら、入力処理が走るまで時間がかかる。
画面更新も完全に止まる。
パッと見で処理落ちが分かったり、場合によっては操作不要になる。
最近よく見る「ロード中」なんて画面中央でグルグルするやつなんかであれば、グルグルさせるための処理を走らせることすらできなくなる。
つまり?
ロード中画面は動かない静止画面になっちゃうし、カーソルは砂時計状態。
CSS側でアニメーションさせてたら関係ないけどね。
あくまでJavaScriptでロード用アニメーションを制御してた場合の話。
これからまだまだ処理が増えていく予定だし、描画処理もスレッドを分けてしまいたい。
ところが、WebWorkerでは今回描画に使っているCanvasを扱えない。
代わりにほぼほぼ同機能を持つ、OffscreenCanvasというものを代わりに使えば描画できる。
OffscreenCanvas - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/OffscreenCanvas
参考サイトも張っとく。
オフスクリーンキャンバスを使ったJSのマルチスレッド描画 - スムーズなユーザー操作実現の切り札 - ICS MEDIA
https://ics.media/entry/19043/
これでゲロ重処理を分散できるぜぇー!!
…と思いきや。
これ、あくまで実験的な機能らしく、対応していないブラウザもめっちゃある。
#ブラウザが対応してないかもしれない問題どうするよ?
元々は動いてたのに、WebWorkerを導入したせいで動かなくなる、というのは忍びない。
あくまで処理速度(しかもメインはロード速度)を上げるためだけのものだから。
注目すべきはWebWorkerとメインスレッドとの連携方法。
postMessage()を使った、メッセージのやり取りによるもの、なんだけども…
やってることは結局、messageイベントの発行だったりする。
え?イベントの発行自体は、大抵のブラウザでもできるよね…?
【GitHub】path_control/path_main.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/path_main.js
22行目から。
postMessage: function(obj, opt) {
if(PathMain.useWorker) {
PathMain.worker.postMessage(obj, opt);
} else {
window.dispatchEvent(new CustomEvent("message", {bubbles: true, detail: obj}));
}
},
いやぁ、いつ見ても酷いねコレ。
PathMain.useWorkerは
!!Worker && !!canvas.transferControlToOffscreen
の結果が入ってる。つまり…
Workerというものが存在していて、
かつOffscreenCanvasのインスタンスを取得するためのtransferControlToOffscreen()が存在していれば、trueになる。
これで、このブラウザがOffscreenCanvasに対応してるかチェックしてる。
で、いけるんであれば元々予定していたPathMain.worker.postMessage()
を使う。
んじゃ、対応してなかったら?
この場でmessageイベントを発行してる。
…ん?
実は、WebWorkerの初期化時の処理が分岐していて…
205行目から。
if(PathMain.useWorker) {
PathMain.worker = new Worker(filePath);
if(!!jsPath) {
PathMain.postMessage({
cmd: "set-control",
path: new URL(jsPath, window.location.href).href,
});
}
PathMain.initWorker();
} else {
console.log("this browser is not supported");
PathMain.worker = window;
if(!!jsPath) {
let subScript = document.createElement("script");
subScript.src = jsPath;
document.body.appendChild(subScript);
}
let mainScript = document.createElement("script");
mainScript.src = filePath;
document.body.appendChild(mainScript);
}
if文の中身は、通常のWebWorker初期化処理の部分。
else文の中身では…
用意したjsファイルのパスを、HTMLのタグとして追加してる。
つまり?
<script src="[ファイルパス]"></script>
がHTMLの中に追加される。
ということは?
メインスレッドとして追加で読み込みされる。
なんとまあ、WebWorker用に用意したjsファイルを、そのままフツーのjsファイルとして読み込んでるのだ。
フツーに読み込んでしまえば、あとはフツーのイベント発行をすればいい。
ということでpostMessage()の
window.dispatchEvent(new CustomEvent("message", {bubbles: true, detail: obj}));
が用意されてる。
bubblesオプションは、メインスレッド内に複数存在してしまう
addEventListener("message", ... )
を巡回させるためのもの。
用事のないcmd名が指定されていた場合はスルーするようにしてる。
【GitHub】path_control/PathWorker.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/PathWorker.js
21行目から。
init: function() {
PathWorker.instance.addEventListener("message", function(e) {
let data = !e.data? e.detail : e.data;
switch(data.cmd) {
WebWorkerに対してpostMessage()を呼び出すとdataにメッセージが入ってるんだけど、
dispatchEvent()だとdetailに入ってるので、そこをチェックした上で、
switch文で分岐させて、メッセージ通りの処理を実行する。
我ながらごり押しすぎる。ワロス。
#CanvasとOffscreenCanvasの両方に対応していくスタイルッ
仮にうまくWebWorker内に読み込めたとしても、
WebWorkerからCanvasで直接表示することはできず、OffscreenCanvasを使う必要がある。
ブラウザが対応してないなら、従来通りCanvasで表示する必要がある。
とはいえ、OffscreenCanvasの機能はほぼCanvasと同じ。
じゃあ同じ変数に入れてもいいよね?と。
【GitHub】path_control/path_main.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/path_main.js
125行目から。
let targetCanvas = PathMain.canvas;
let targetSubCanvas = PathMain.subCanvas;
if(PathMain.useWorker) {
targetCanvas = targetCanvas.transferControlToOffscreen();
targetSubCanvas = targetSubCanvas.transferControlToOffscreen();
}
PathMain.postMessage({
cmd: "init",
viewWidth: viewWidth,
viewHeight: viewHeight,
canvas: targetCanvas,
subCanvas: targetSubCanvas,
defaultBoneName: PathMain.defaultBoneName,
}, [ targetCanvas, targetSubCanvas ]);
PathMain.canvasも、PathMain.subCanvasも、
document.createElement("canvas")
で作ったCanvasタグで、subCanvas側は非表示したまま描画処理をしておいて、表示するタイミングでsubCanvasの内容をcanvasにコピペするようにしてる。
いわゆるダブルバッファリングってやつ。これで一応ちらつき防止になるのだ。
で、transferControlToOffscreen()を使うことで、各々に対応したOffscreenCanvasを生成できる。
あとはcanvasであれ、OffscreenCanvasであれ、postMessage()で投げつけてWebWorkerの初期化処理をする、と。
よく見るとpostMessage()の第2引数に
[ targetCanvas, targetSubCanvas ]
がついてる。ここもポイント。
postMessage()で渡したデータはあくまでメッセージとしてコピーで生成されたものなので、通常共有されない。
だから渡したデータは「同じ内容の違うデータ」なわけだ。
なので、受け取った側でいくらいじっても、送った側のデータに変化は起こらない。
一方、この第2引数で指定されたデータは、WebWorkerとメインスレッドで共有して扱うことができる。
ただし、なんだって共有できるわけじゃない。
OffscreenCanvasが渡せるデータの一つというだけである。
#やべ、外部から制御する処理どこに用意しよう?
元々は、読み込ませたSVGによって追加で制御したい処理があったときに、JavaScriptで追記する形にしようかと思ってたわけで。
例えば、徐々に横に移動させる処理を追加するとか、回転させる処理を追加するとか。
カーソルで制御するか、キーボードで制御するか、とか。
そういうの。
ところがぎっちょん。
データをWebWorkerで持たせてしまうと、HTML側やHTMLで読み込んだJavaScript側の処理では、WebWorker内のデータにアクセスできない。
でも、WebWorkerにはimportScripts()という、新しいjsファイルを読み込む機能が、あるにはある。
んじゃこれを使おう。
もひとつぎっちょん。
しまった、WebWorker内だとカレントディレクトリが変わるんだった。
相対パスのままじゃ渡せないな…
ということで、206行目から。
PathMain.worker = new Worker(filePath);
if(!!jsPath) {
PathMain.postMessage({
cmd: "set-control",
path: new URL(jsPath, window.location.href).href,
});
}
PathMain.initWorker();
用意したWebWorkerにpostMessage()で指定されたjsファイルのパスを渡す。
この際に、今開いてるHTMLのURL(window.location.href)をベースとした絶対パス(URL)へ変換しておく。
あとは、受け取ってimportScripts()するだけ。
【GitHub】path_control/PathWorker.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/PathWorker.js
80行目から。
case "set-control":
importScripts(data.path);
これで追加のjsファイルをWebWorker内で実行できる。
好き放題いじれる。やったぜ。
次の記事:【SVG制御妄想7】SVGの限界
https://qiita.com/flying_echidna/items/2f53a461c5e6c05109df