LoginSignup
5
4

More than 3 years have passed since last update.

JavaScriptで画像を格子状に並べて結合する

Last updated at Posted at 2020-07-23

前置き

某ソシャゲのキャラ立ち絵を集めてたら、一覧画像を作りたくなった。
手慣れたPythonでアプリ作ってたけどおいおいGUI上での任意の並び替え機能も盛り込むために、HTMLとJavaScriptに移行してみたのが始まり。

やりたかったこと

任意の枚数の画像を選択し、全体が大体正方形になるように自動で並べる。
画像の解像度が違う場合は最も大きいものに合わせる、必要に応じて縦横比も変える。

開発環境

Mery(x86) 2.5.6
Firefox 78.0.2(64ビット)

結果

コード

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JavaScriptで画像を格子状に結合</title>
</head>
<body>
<form>
<input type="file" accept='image/*' onchange="AlignImage(this);" multiple="multiple">
</form>
<p>
Preview:<br>
<canvas id="preview" width="600" height="400" style="background-color:black;"></canvas>
<script>
function AlignImage(obj)
{
    var images = [];
    var cnt_loaded = 1;
    var cnt_added = 0;
    for (const file of obj.files) {
        const no = cnt_added;
        const reader = new FileReader();
        reader.onload = (function() {
            images[no] = new Image();
            images[no].src = reader.result;
            console.log('no: ' + no);
            console.log('src: ' + reader.result);
            console.log('cnt_loaded: ' + cnt_loaded);
            console.log('images.length: ' + images.length);
            images[no].onload = (function () {
                console.log( no + ' is loaded.');
                console.log('cnt_loaded: ' + cnt_loaded);
                console.log('obj.files.length: ' + obj.files.length);
                if (cnt_loaded == obj.files.length) {
                    var canvas = document.getElementById('preview');
                    var ctx = canvas.getContext('2d');
                    var w_max = 0;
                    var h_max = 0;
                    for (const image of images) {
                        if (image.width * image.height > w_max * h_max) {
                            w_max = image.width;
                            h_max = image.height;
                        }
                    }
                    var row = images.length;
                    var col = 1;
                    while (row != col && row * h_max > col * h_max) {
                        col++;
                        row = Math.ceil(images.length / col)
                    }
                    console.log('w x h: ' + w_max * col + ' x ' + h_max * row);
                    console.log('col x row: ' + col + ' x ' + row);
                    canvas.width = w_max * col;
                    canvas.height = h_max * row;
                    images.forEach(function(image, i){
                        ctx.drawImage(image, 0, 0, image.width, image.height, w_max * (i % col), h_max * Math.floor(i / col), w_max, h_max);
                    })
                }
                cnt_loaded++;
            });
        });
        reader.readAsDataURL(file);
        cnt_added++;
    }
}
</script>
</body>
</html>

画面

Screenshot_2020-07-24.jpg
写真は四国旅行のもの。

苦労したところ

それぞれのimageのonloadが発火するタイミングが分からん

最も解像度が高い画像を特定するために、こちらの記事を参考に全ての画像を読み込めた時に発火するよう下記のonloadイベントを登録していた。

images[no].onload
images[no].onload = (function () {
    if (cnt_loaded == images.length) {
        #canvasへの書き出しロジック
    }
    cnt_loaded++;
});

しかし2枚目以降のimageオブジェクトがimagesへ追加される前に1枚目のonloadが発火してしまうせいで、1枚だけ出力されてしまっていた。
よって最初から枚数が格納されているobj.files.lengthcnt_loadedを比較するよう修正した。

for内のvarで宣言した変数が次の繰り返し時に書き換わってしまう

onloadが発火するタイミングが分かってなかったのとvarのスコープを分かってなかった。
各画像のインデックスと、base64形式へ変換するFileReaderオブジェクトをリセットするために、下記のようにfor内でnoreaderをvarで定義していた。

for
for (const file of obj.files) {
    var no = cnt_added;
    var reader = new FileReader();
    reader.onload = (function() {
        #base64への変換
        images[no].onload = (function () {
            #canvasへの出力
            }
            cnt_loaded++;
        });
    });
    reader.readAsDataURL(file);
    cnt_added++;
}

しかし実際にnoreaderを使用するreader.onloadimages[no].onloadイベントが発火する頃にはforは次の周回に入っており、noreaderは置き換わってしまう。
結果エラーだったり出力がめちゃくちゃになっていた。
これはvarからconstに変えることで対応した。正直上手く行ってる理由が分かってないが、おそらくスコープが限定されたことによるものだと理解している。

今後実装したい機能

出力解像度の指定ファイルサイズを指定できるようにしてみた
行・列数の操作
画像のドラッグ&ドロップによる任意の並び替え

参考

https://blog.ver001.com/javascript_preview_canvas/
https://qiita.com/akase244/items/9f3c60219f903254a6e2
https://qiita.com/mochizukikotaro/items/ff6cd42e902e79b02649
https://maku77.github.io/js/array/loop.html
https://techacademy.jp/magazine/14872
http://www.htmq.com/canvas/drawImage_s.shtml
http://www.htmq.com/canvas/fillRect.shtml
https://eng-entrance.com/javascript-array-length
https://kazumich.com/html5multiple.html

5
4
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
5
4