意外と簡単!Laravelでgifアニメをつくる

こんにちは!フリーランス・エンジニアの 九保すこひ です。

さてさて、前回記事Laravelでwebp形式の画像をつくる&使うではwebpというGoogleが開発した新しい&軽量な画像をつくってみました。

そして、この画像に関する流れでもう一つある機能をLaravelでつくってみたくなりました。

それは・・・

gitアニメをつくる機能

です。

「gifアニメ」とは、内容が動いて動画のように見える画像のことです。
おそらく皆さんも一度はみたことがあると思います。

こういうやつですね↓↓↓

gifアニメはファイルサイズ的にいうとそれほど大きくないので、「動画を使うまではしなくていいけど、何か動きがほしい」という場合には重宝されると思います。

そこで!

今回はLaravel + Vueを使ってgifアニメを作る方法をご紹介します。

ぜひ皆さんのお役に立てると嬉しいです😊✨
(最後に実際に開発したソースコードをダウンロードすることができますよ)

開発環境: Laravel 6.x、Vue 2.6

やりたいこと

ただ単にgifアニメをつくるだけでは面白くないので、今回は次の手順で実装してみます。

  1. ブラウザから複数の画像を選択&プレビュー
  2. 画像を送信
  3. 送信された画像でgifアニメを作成
  4. 最後に作成されたgifアニメを表示

では実際にやっていきましょう!

フォルダを準備する

今回作成するgifアニメ画像は、/public/images/gifフォルダの中に保存します。そのため、このフォルダを作成し、書き込み権限を与えておいてください。

画像を用意する

gifアニメにする画像を用意します。

今回はLINEスタンプで販売している画像を改造して以下の6つの画像で作成します。(皆さんも適当な画像がない場合、テストとして使ってください😊👌)

パッケージをインストールする

gifアニメをつくるためのパッケージが公開されていますので先にcomposerでインストールしておきましょう。

composer require sybio/gif-creator

ルートをつくる

ではここからが実際にプログラムを書いていく作業になります。

今回必要なルートは以下の2つです。
routes/web.phpに追加しておいてください。

Route::get('animated_gif/create', 'AnimatedGifController@create');
Route::post('animated_gif', 'AnimatedGifController@store');

上のルートが実際にブラウザからアクセスするURLで、下がAjaxで画像を送信するURLになります。

コントローラーをつくる

続いてコントローラーです。
AnimatedGifControllerという名前の専用コントローラーをつくるので、以下のコマンドを実行してください。

php artisan make:controller AnimatedGifController

すると、app/Http/Controllers/AnimatedGifController.phpというファイルが作成されるので中身を以下のように変更してください。

<?php

namespace App\Http\Controllers;

use GifCreator\GifCreator;
use Illuminate\Http\Request;

class AnimatedGifController extends Controller
{
    public function create() {

        return view('animated_gif.create');

    }

    public function store(Request $request) {

        $result = false;
        $gif_url = '';

        if($request->hasFile('images')) {

            $frames = [];       // 画像のコマ
            $durations = [];    // 画像が変化する間隔(ミリ秒)
            $loop = 0;          // 何回ループするか。0は無限

            foreach($request->images as $image) {

                if($image->isValid()) {

                    $frames[] = $image->path();
                    $durations[] = 20;

                }

            }

            $gc = new GifCreator();
            $gc->create($frames, $durations, $loop);
            $gif_data = $gc->getGif();

            $uri = 'images/gif/'. date('U') .'.gif';
            $save_path = public_path($uri);
            file_put_contents($save_path, $gif_data);

            $result = true;
            $gif_url = url($uri);

        }

        return [
            'result' => $result,
            'gif_url' => $gif_url
        ];

    }
}

create()は画像を選択するフォームを表示するメソッドで、store()が画像が送信されてくるメソッドになります。

重要なのはstore()ですが、この中では単に送信されてきた画像データをGifCreator()のインスタンスに1つずつ登録し、最終的にgetGif()でgif画像のデータを取得&保存しているだけです。

ビューをつくる

では最後にビューを作成します。
resources/views/animated_gif/create.blade.phpというファイルを作成し中身を以下のように変更してください。

※ ちなみに今回は比較的新しいコードの書き方を多めに使ってみました。これを機にぜひ勉強してみてください😊

<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
    <style>

        img {

            border: 3px solid #000;

        }

    </style>
</head>
<body class="p-4">
    <div id="app" class="container">
        <!-- 画像を選択する部分 -->
        <div class="row">
            <div class="col-12">
                <div class="form-group">
                    <label>gifアニメにする画像を選んでください(複数)</label>
                    <input class="form-control" type="file" accept="image/*" multiple @change="onFileChange">
                </div>
            </div>
        </div>
        <!-- 選択された画像のプレビュー部分 -->
        <div class="row">
            <div class="col-4 p-3" v-for="(image,index) in images">
                <div>
                    <span v-text="(index+1)"></span>コマ目
                </div>
                <img class="img-fluid" :src="image.data">
            </div>
        </div>
        <!-- 画像が選択されたら表示される部分 -->
        <div class="row" v-if="images.length">
            <div class="col-12 pt-4">
                <button class="btn btn-primary" type="button" @click="onSubmit">画像をアップロードする</button>
            </div>
        </div>
        <!-- gifアニメが完成したら表示される部分 -->
        <div v-if="gifUrl" class="pt-4">
            <img :src="gifUrl">
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                images: [],
                gifUrl: ''
            },
            methods: {
                onFileChange(e) {

                    this.images = [];
                    const files = e.target.files;
                    let promises = [];

                    [].forEach.call(files, file => {    // 選択された画像をループ

                        let promise = new Promise(resolve => {

                            const reader = new FileReader();
                            reader.onload = e => {

                                const imageData = e.target.result;

                                // Promiseに画像データを返す
                                resolve({
                                    file: file,
                                    data: imageData
                                });

                            };
                            reader.readAsDataURL(file);

                        });
                        promises.push(promise);

                    });

                    Promise.all(promises).then(images => {

                        // ファイル名で並び替え
                        images.sort((a, b) => (a.file.name > b.file.name) ? 1 : -1);
                        this.images = images;

                    });

                },
                onSubmit() {

                    const url = '/animated_gif';
                    let formData = new FormData();

                    this.images.forEach(image => {

                        formData.append('images[]', image.file);

                    });

                    axios.post(url, formData)
                        .then(response => {

                            if(response.data.result) {

                                this.gifUrl = response.data.gif_url;

                            }

                        });

                }
            }
        });

    </script>
</body>
</html>

では、ひとつずつ説明していきます。

画像を選択&プレビュー表示する部分

画像が選択されたときに実行されるのが、onFileChange()です。

この中では、選択された画像を[].forEach.call()で1つずつループし、画像データを読み込むためにPromiseをつくっています。

Promiseは配列になってpromisesに格納され、最終的にPromise.all()の中で実行される(つまり、ここで画像データが読み込まれる)ことになります。

Promise.all()はイメージとしては、「チームとして結果がどうなったか」でその後の処理を判断するもので、例えば野球で考えると分かりやすいかもしれません。

  1. 打順1番の選手から順に打席にたつ
  2. ヒットがでたら、次の選手が打席へ(←繰り返す)
  3. ただし、ひとりでもアウトになった時点で試合は負け

つまり、全ての人がヒットを打つことができればPromise.all()は「成功」、一人でもアウトになったら「失敗」という判断になります。そしてPromiseの中で成功、失敗を決めるのはresolve()reject()です。

let promise = new Promise((resolve, reject) => {

    // 処理が成功したら
    resolve('データ');

    // 失敗したら
    reject('(データ)');

});

そして、失敗した場合はcatch()の方が実行されることになります。

Promise.all(promises).then(data => {

    // すべて成功した場合

})
.catch(data => {

    // ひとつでも失敗した場合

});

なお、なぜこのような形でPromiseを使っているかというと、「画像の読み込みはいつ完了するかわからないから」です。

つまり、FileReaderonloadが実行された時点で画像データを集めていくわけですが、画像によってはサイズが小さくすぐ読み込めるけれども、別の画像は読み込みに時間がかかる場合があるため、わざわざPromise.all()で全ての処理が完了してから(チームとしての結果がわかってから)次の処理に移っているわけです。

プレビュー画像を表示する部分

Promiseを使って取得した画像データを直接<img>タグのsrcにセットすると画像が表示されますので、これをプレビュー画像をして使っています。

<div class="col-4 p-3" v-for="(image,index) in images">
    <div>
        <span v-text="(index+1)"></span>コマ目
    </div>
    <img class="img-fluid" :src="image.data">
</div>

画像を送信する部分

選択された画像をAjaxで送信するためにaxiosを使っていますが、前回記事と同様にFormDataを使っていることに注目してください。(通常のパラメータとしては送信できません)

なお、画像を送信しgifアニメの作成が成功したらjsonで以下のようなデータが返ってくることになります。

{
    "result": true,
    "gif_url": "(gifアニメのURL)"
}

テストしてみる

では、実際テストしてみましょう!

まずはページを表示して画像を選択します。

すると、選択された画像のプレビューが表示されるので、送信ボタンをクリックします。

すると、作成されたgifアニメがボタンの下に表示されました!

実際に作成された画像がこちらです。

成功です😊✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

※ ただし、フォルダや送信する画像の用意、パッケージのインストールなどはご自身で行ってください。

Laravelでgifアニメをつくる

おわりに

ということで今回はLarave + Vueでgifアニメをつくってみました。

いろいろなテクノロジーを組み合わせると意外と簡単にいろいろな機能が実装できることを知ってもらえると嬉しいです。

なお、JavaScriptの新しい書き方、Promise[].forEach.call()はいかがだったでしょうか。IE 11には対応していませんが、どんどんIEは過去のものになっている(EdgeChromeベースになりますしね😊)ので、これからは主流になってくるのは間違いないかと思います。

また、JavaScriptはブラウザだけでなくnodejsとしてサーバーサイドの開発にも使えるようになりましたので、そういった部分でも覚えておいて損はしないんじゃないでしょうか。

ぜひ皆さんも今回の内容を参考にしてみてくださいね。

ではでは〜♪

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
このエントリーをはてなブックマークに追加       follow us in feedly