hkのweblog

ひよっこエンジニアがにわとりになるまでの軌跡

canvasで画像ファイルを読み込む

プロ野球シーズンは家でコードを書く時間が少なくなりがちです。うーん。
ということで1ヶ月半ぶりに記事を書きます。


先日、html5で導入されたcanvasを使った実装をしたので、そのコードを残しておこうと思います。
公式のドキュメントはこちら↓
Canvas API - HTML: HyperText Markup Language | MDN

canvasを使うのは初めてで、ユーザーがローカルの画像ファイルをアップロードする機能を実装したのですが、なかなか楽しかったです。
この記事では選択した画像を表示し、その上に別の画像をかぶせてみます。
Vue.jsで画像をあげる部分だけをコンポーネント化したので、そのコンポーネントをまるっと載せてみます。

<template>
    <div>
        <label>写真:</label><br>
        <canvas id="canvas" height="0"></canvas><br>
        <label>
            <span class="btn btn-primary btn-sm">
                画像を選択
                <input type="file" name="photo" v-on:change="drawCanvas" style="display:none">
            </span>
        </label>
        <div id="result"></div>
    </div>
</template>

<script>
export default {
    methods: {
        drawCanvas(e) {
            let fileData = e.target.files[0]
            if (!fileData.type.match('image.*')) {
                alert('画像を選択してください')
                return
            }
            let canvas = document.getElementById('canvas');
            let canvasWidth = 400;
            let canvasHeight = 300;
            canvas.width = canvasWidth;
            canvas.height = canvasHeight;
            let ctx = canvas.getContext('2d');

            let reader = new FileReader();
            let that = this;
            reader.onload = function() {
                let uploadImgSrc = reader.result;
                // canvas上に画像を重ねて表示
                let img = new Image();
                img.src = uploadImgSrc;
                img.onload = function() {
                    ctx.drawImage(img, 0, 0, canvasWidth, this.height * (canvasWidth / this.width));
                    // imgをloadした後にframe.jpgをloadして乗せる
                    let frame = new Image();
                    frame.src = "img/frame.jpg";
                    frame.onload = function() {
                        ctx.drawImage(frame, 0, 200, 400, 100);
                        // canvasを画像に変換
                        let imgSrc = canvas.toDataURL();
                        that.$store.commit('setImage', imgSrc)
                    }
                }
            }
            reader.readAsDataURL(fileData);
        }
    }
}
</script>

画像が選択されるとdrawCanvasが呼ばれます。
最初に選択されたファイルが画像かどうかチェックしています。本当はサイズ等々もう少し丁寧にバリデーションをかけるべきでしょうね。

次にcanvasのサイズ等基本的な設定をしていきます。
canvas要素はデフォで中途半端に高さを取ってしまうようです。
<canvas id="canvas" height="0"></canvas>のように書いておくと余計な隙間をなくすことができます。
getContextのところで2dを指定していますが、3次元の画像もできるようです。canvasを直接書くのではなく、three.js等のライブラリを使ったほうが良いようです。いつかやってみたい。

次にFileReaderオブジェクトを生成し、読み込んだ画像をImageオブジェクトにあてていきます。
ctx.drawImage(img, 0, 0, canvasWidth, this.height * (canvasWidth / this.width)); 左上を基点とし、右に0、下に0の位置から、幅がcanvasWidth、高さをthis.height * (canvasWidth / this.width)の画像を描画しています。
高さ300幅400の領域に幅固定で高さは縦横比を崩さずに画像を読み込んでいるということになります。
同様に上からかぶせる画像frame.jpgを読み込んでいます。ここではユーザーが読み込んだ画像の下部にバナーのような感じでframe.jpgが乗ることになります。
このように入れ子にしてやることで必ずframe.jpgが後から読み込まれて上に乗るようになります。

最後にreader.readAsDataURL(fileData);で画像を表示しています。
読み込むとこんな感じになります↓ f:id:h2r4t:20180706230431p:plain
なお、その上の2行ではcanvasに表示したものを1枚の画像URLに変換しています。
自分の場合はこれを別コンポーネントに渡して表示したり保存したりしたかったので、Vuexを使って渡しています。

以上、雑な実装ですが誰か読んだ方の参考になれば。