Electron でアニメGIFをつくるアプリ(ダウンロード可)

さてさて、これまでウェブかコマンドラインばかりの開発だったのがElectronでデスクトップアプリを開発をするようになって、まだまだ試しにやってみたいことが思い浮かんできています。

特にElectronはアップロードをせずにデスクトップ上のファイルにアクセスができるので、画像やテキストなどファイル関連の機能と親和性が高いのかなと感じています。

そこで、今回開発するアプリは「アニメGIFをつくる」アプリです。

アニメGIF(animated gif)とは、動画のように動かすことができる画像のことで、次のようなものです。インターネット上でも珍しくないのでみなさんも一度は見たことがあるんじゃないでしょうか。

このアニメGIF、「動きを見せたいんだけど、JavaScriptとか動画にするまででもないよな・・・」なんてときに使えます。しかも動画ほどファイルサイズが大きくもないので配布するにも都合が良かったりします。

ということで、今回はアニメGIFを作ることができるアプリを開発してみます!

※ 開発環境: Electron、ImageMagick 6.9

やりたいこと

アニメGIFをつくるといっても、せっかくElectronでアプリを作るので、次の機能もつけることにしました。

  • 画像をアプリから選択できる
  • 切り替わる画像の順番をドラッグ&ドロップで変更できる
  • 画像が切り替わる間隔を指定できる

の3つです。

準備

今回、実際アニメGIFの作成はImageMagickを使います。
そのため、事前にImageMagickが使えるようインストールしておいてください。

私の同じくUbuntu環境でしたら以下のコマンドでインストールできます。

sudo apt install imagemagick

Electronのインストール

インストールは、初心者向き!electron で簡単なメモ帳をつくってみよう“electronのインストール” に詳しく書いてあるので、そちらを見てください。

フォルダ名はanime-gif-makerです。

アプリの設定

main.jsを開いてウィンドウサイズ、位置、メニューバーの非表示を設定します。

mainWindow = new BrowserWindow({
    width: 500,
    height: 500,
    x: 0,
    y: 0,
    autoHideMenuBar: true
})

また、私の環境のようにLinuxを使っているとアプリ上でドラッグ&ドロップするとクラッシュしてしまうバグがあるので、app.disableHardwareAcceleration()を使ってこれを回避しておきます。(どうやらハードウェア・アクセラレーションが原因のようです)

const {app, BrowserWindow} = require('electron')

app.disableHardwareAcceleration()

CSS、JSのフレームワーク

レイアウト用にbootstrap、JavaScript用にVue、そして要素をドラッグ&ドロップで並べ替えることができるsortablejsをインストールします。

npm i bootstrap --save
npm i vue --save
npm i sortablejs --save

レイアウトをつくる

では、bootstrapを使ってレイアウトをつくります。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>アニメGIFをつくるアプリ!</title>
    <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css">
    <style>

        body {
            padding: 15px 0;
        }

        #image-container {
            background: #eee;
            padding: 15px;
            border-radius: 5px;
            height: 300px;
        }

        #time {
            width: 200px;
        }

        #button {
            width: 150px;
            position: absolute;
            right: 15px;
            bottom: 20px;
        }

    </style>
</head>
<body>
<div id="app" class="container">
    <select id="time" class="form-control float-right">
        <option>切り替わり(1/100秒)</option>
    </select>
    <label class="btn btn-light">
        画像を追加<input type="file" accept="image/*" multiple hidden>
    </label>
    <hr>
    <label>表示順を並べ替える(ドラッグ&ドロップできます)</label>
    <div id="image-container">
        (ここに画像リスト)
    </div>
    <button id="button" type="button" class="btn btn-dark">アニメGIFをつくる</button>
</div>
<script>
    require('./renderer.js')
</script>
</body>
</html>

まず画像を選択するボタンですが、通常の<input type="file">ではなく、ボタンをクリックすることで「ファイル選択ダイアログ」が表示されるテクニックを使っています。

また、ファイル選択できるのはaccept="image/*"で画像だけに制限し、multipleで複数選択を許可しています。

そして、<select>で画像が切り替わる間隔を指定できるようにし、選択された画像のリストはドラッグ&ドロップで順番を入れ替えられるようにしています。

実際の画像はこちらです。

では、JavaScript部分をつくっていきましょう!

JavaScript部分をつくる

Vueの基本形をつくる

まずはVueを読み込んで基本形をつくります。

<script src="./node_modules/vue/dist/vue.min.js"></script>
<script>

    new Vue({
        el: '#app'
    })

</script>

画像を選択する部分をつくる

次に画像の選択部分です。
<input type="file">にファイルが選択されたときに実行するsetImages()をクリックイベントとして設定します。

<input type="file" accept="image/*" @change="setImages($event)" multiple hidden>

そして、dataに選択された画像ファイルのデータが入るfilesを追加し、さらにmethodsにもsetImage()を追加します。

data: {
    files: []
},
methods: {
    setImages(e) {
        this.files = e.target.files
    }
}

これで、ファイルが選択されたら自動的にfilesにデータが反映されることになります。

切り替わり時間の部分をつくる

次に切り替わる間隔を選択するセレクトボックスの部分です。

まずdataへ時間を格納するtimeを追加。
v-modelでバインディングします。

data: {
    files: [],
    time: 10
}

そして、いつもならtimeOptionsなどとして選択肢も追加するのですが、今回はv-forの数字バージョンを使うことにします。

<select id="time" class="form-control float-right" v-model="time">
    <option v-for="i in 10" :value="i*10">切り替わり({{ i/10}} 秒)</option>
</select>

実行したものがこちらです。

画像が表示される部分をつくる

画像がドラッグ&ドロップで並べ替えられるようにしたいので、先ほどインストールしたsortablejsを読み込みます。

window.Sortable = require('sortablejs')

※ なお、通常の<script src="***">ではうまくいきません。

次に、画像をv-forでレンダリングします。

<div id="image-container">
    <img v-for="(file,index) in files"
         :src="file.path"
         :data-index="index"
         class="image">
</div>

data-indexの部分は、後で画像の順番を決める時に使います。

また、画像が選択されたらimage-containerSortableを設定します。

setImages(e) {
    this.files = e.target.files
    Vue.nextTick(() => {
        const el = document.getElementById('image-container')
        Sortable.create(el)
    })
}

※ なお、Vue.nextTick()を使っているのは、確実に表示がVueによって変更された後でSortableを設定するためです。

では、一旦どんな風に表示されるかをチェックしてみましょう。

うまくいきました!
ドラッグ&ドロップで画像を移動させることもできます。

アニメGIFをつくる部分をつくる

では、やっと本題にたどりつきました。
「アニメGIFをつくる」ボタンがクリックされたときのコードをつくっていきます。

まずはファイル操作をするモジュールfs、コマンドを実行するspawnが使えるように読み込みます。

const fs = require('fs')
const {spawn} = require('child_process')

なお、ImageMagickのコマンドではファイル名が連番になっている必要があるため、tmpというフォルダを作っておき、ここへ選択された画像のコピーを作ってからコマンドを実行するようにします。

そして、generateAnimatedGif()が実行されるクリック・イベントです。

<button 
    id="button" 
    type="button" 
    class="btn btn-dark" 
    @click="generateAnimatedGif()">アニメGIFをつくる</button>

generateAnimatedGif()、その中で呼び出すrefreshTempFiles()makeTempFiles()はこうなります。

generateAnimatedGif() {
    this.refreshTempFiles()
    this.makeTempFiles()
    const now = Date.now()
    const tmpPathPattern = __dirname +'/tmp/*.png'
    const outputPath = `${__dirname}/animated_${now}.gif`
    const command = `/usr/bin/convert -delay ${this.time} -loop 0 -alpha remove ${tmpPathPattern} ${outputPath}`
    const process = spawn('/bin/sh', ['-c', command])
    process.on('close', (code) => {
        if(code == 0) {
            alert('アニメGIFが作成されました!');
        }
    });
},
refreshTempFiles() {
    const tmpPath = __dirname +'/tmp/'
    fs.readdirSync(tmpPath)
        .forEach((file) => {
            fs.unlinkSync(tmpPath + file)
        })
},
makeTempFiles() {
    const el = document.getElementById('image-container')
    const images = el.childNodes
    images.forEach((image, i) => {
        let index = image.getAttribute('data-index')
        let file = this.files[index]
        let filename = i.toString().padStart(6, '0') +'.png'
        let tempPath = __dirname +'/tmp/'+ filename
        fs.copyFileSync(file.path, tempPath)
    })
}

まずrefreshTempFIles()tmpフォルダ内の全ファイルを削除します。

そして、makeTempFiles()image-containerの中にある画像をchildNodesで取得し一時ファイルを連番で作成していきます。

さらに、最後にspawnでコマンドを実行しますが、ここで重要なのが/usr/bin/convert/usr/shなどのパスです。

これらは、which shwhich convertなどでコマンドを実行すると表示される絶対パスです(Windowsの場合はwhere)。パスが通っていれば問題ありませんが念の為この形にしています。各自環境に合わせて変更してください。

なお、今回のconvertコマンドには-alpha removeをつけて透過部分を削除するようにしています。

では、実際に今回のアプリを使って作成したアニメGIFをご覧ください。

↓↓↓

お疲れ様でした!

教材ソースコードをダウンロードする

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

※ ただしnpmのインストールやアニメGIF化する画像はご自身で用意してください。

Electron でアニメGIFをつくる