Aikの技術日記

技術的な進捗とか成果とかを細々と投稿するブログです。時々雑記も。

Electronでぷいぷいしたデスクトップマスコット作ってみた

はじめに

こんにちは、筆者です。
今季の冬アニメシーズンも終わりかけ…今季推しは何個ほどあったでしょうか。
筆者的には今季冬アニメ当たり度は大きかったです。なんせ『SK∞(エスケーエイト)』に『アズールレーン びそくぜんしんっ!』、更には『呪術廻戦』の2期までありましたからね!

そんな中でも、特に筆者のとびきりお気に入りなのがこちらのアニメ『PUI PUIモルカー』です: molcar-anime.com

Twitterでもよく話題となっていたので、知ってる方も多いのではないでしょうか。
毎週火曜日にYouTubeで無料公開される本編が楽しみでたまらない日々を送っています。

特にもしゃもしゃ燃料補給していたり、だばだば走ったりしている時が可愛すぎてですね…。

上記にある様にファンアートが捗りまくる日々…いやぁすばらしいですね。

しかし…最近「ファンアートだけでは物足りなく」なってきまして。
「もっと手軽にモルカー成分を摂取したい!」→「そうだ! デスクトップマスコットにしよう!」と思い立ち。
善(?)は急げ、PUI PUIモルカーの非公式デスクトップマスコットを作るに至った次第です。

Githubに公開中なので、気になった方はぜひ: github.com

本ブログでは、こちらのデスクトップマスコットを作るにあたり:

  • 使用した技術
  • 制作の経緯

…を筆者の備忘録代わりに書いていこうと思います。

それではいきましょう。

使用した技術

今回デスクトップマスコット作成にあたり使用した技術はElectronです。
HTML, CSS, JavaScriptといったWebフロントエンド周りの技術をベースにコードを書ける上、WindowsOS、Mac OS、LinuxOSといった主要OS向けのデスクトップアプリを1つのコードで作れるやつですね。
Electronについてもっと知りたい方は、手前味噌になりますがこちらの記事の余談にて詳しく書いております。もしよければ。

ちなみになんでElectronを選定したのかというと、単純に「筆者が一番得意な技術分野がフロントエンド系に固まっているから」です。
というか他技術の習熟度が浅すぎてですね…(白目)

また、ElectronだけだとHTML&JavaScriptで処理を書く羽目になります。
デスクトップマスコットの開発規模とはいえ、今後盛り込みたい機能が増えるかもしれないと考えると、何かしらのフレームワークで作りたい…。

と言うわけで、筆者が手慣れているVue.jsを基盤に敷くことにしました。
筆者のVue.js知識量はチョットワカル程度、まだまだ学びたいと思ってたところでしたのでちょうどよかったです。
なお、今回の記事ではVue.jsではなくElectronに関する知識や学んだことが中心なのでご留意を…。

他にもモルカーのパラパラアニメーションを実装するために、CSSアニメーションを使用しました。
さてお次は「制作の経緯」について述べていきます。

制作の経緯

今回のデスクトップマスコットの制作の経緯はこんな感じでした:

  • Electronプロジェクト作成
  • デスクトップアプリ化
    • 背景を透明化させる
    • マウスのクリックを無効化する
    • フレームを非表示にする
    • 用意したアニメーション画像を出力する
  • アイコンを作成&アプリをパッケージング

それでは最初から順に見ていきましょう。

Electronプロジェクト作成

今回は「新規にVue.jsプロジェクトを立ち上げ」「それをElectron化する」方式を取りました。
なので、新規にVue.jsプロジェクトを立ち上げ:

$ vue create molmot-desktop-mascot

その後electron-builderを使用しElectronプロジェクト化します:

$ vue add electron-builder

なお、Choose Electron Versionでは^9.0.0Add tests with Spectron to your project?ではnoを選んでおきました。

これでElectron立ち上げコマンドを叩けば…:

$ yarn electron:serve

Vue.jsのプロジェクトがElectronとして生まれ変わりました!

Vue.jsスタートアップ画面@Electron版

また、この段階でパッケージング化(アプリケーションとして出力)のテストもやってみました。
Mac OS環境下であれば、出来上がったアプリケーションはout/[package.jsonのnameにある文字列]/[package.jsonのnameにある文字列]-darwin-x64内に出力されるようです。

VSCodeだと、アプリケーションがディレクトリとして認識されるのでFinder or Explorerで開いてください。
出来上がったアプリケーションを開き、先ほどローカルで実行した物と同じウィンドウが開けば成功です。

なお、Mac OS環境下からのWindowsアプリケーションのビルドには失敗しました…。
64bit版のWineがあれば良さそうなのですが、構築済みのすぐにインストールできるパッケージは「Mac OS Catalina」非対応とのことだったので…: hirubokari.booth.pm

なんとか自力でインストールしたいと思い色々試しましたが、方法や使い方がわからず苦戦してしまい…。
結局Windows環境を別途用意し、そこでビルドする様にしました。いやぁ大変でしたね…。

背景を透過させる

ビルドまで確認できたと言うことで、ここからはアプリを「デスクトップマスコット」らしくしていきます。
まずは「背景を透過させる」ところから。

Electronの起動設定等を書くbackground.tsにて、下記の様に修正を行います:

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
    transparent: true, // 背景の透明化(ここを追加)
    frame: false, // フレームを非表示にする(ここを追加)
    resizable: false, // ウィンドウリサイズ禁止(ここを追加)
    alwaysOnTop: true, // 常に最前面に表示(ここを追加)
    hasShadow: false, // デスクトップアプリの影をなくす(ここを追加)
  });

  win.loadFile("index.html");
}

上記の様に修正すると、背景が透明化されるのと一緒に:

  • アプリのリサイズが不可能
  • 常に最前面に表示される
  • (Mac OS限定)アプリの影が消える

…といった諸々が適用されます。どれもデスクトップマスコットに相応しい設定かと。
特に最後の「アプリの影が消える」という点は、やっておかないと実際に背景を透過た時にとても気になる所となるので…。

影あり状態のElectronアプリウィンドウ
これが「影あり」状態です

影なし状態のElectronアプリウィンドウ
これが「影なし」状態です

デスクトップマスコットを作る上では必ずつけておきましょう。
Windows限定のアプリにするなら不要です。

…ちなみに一部の環境では、上記設定でも「背景が透明化されない」ケースがあります。
この場合は、「ハードウェアアクセラレーションを無効化」するとよいそうです。
参考記事: Transparent window has white background window · Issue #2170 · electron/electron · GitHub

background.tsapp.on("window-all-closed", () => {と記載されている箇所の上あたりに下記の記述を行えば、無効化できます。

// 背景を透明化するため、ハードウェアアクセラレーションをOFF
app.disableHardwareAcceleration();

マウスのクリックを無効化する

背景を透過させたお次は、「マウスのクリックも無効化」させちゃいましょう。

先ほど記述してたbackground.tsに、下記の様に修正を行いましょう:

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    // ...中略
  });

  // マウスイベントを無視 <-ここだけ追加
  mainWindow.setIgnoreMouseEvents(true);
};

こうすることで、マウスクリックを無効化できます。

フレームを非表示にする

お次はアプリのフレーム(アプリを閉じるボタンや最小化ボタンがある所)を非表示にしていきます。
普通のアプリならある最小化表示ボタンや閉じるボタンがないだけで、グッとデスクトップマスコット感って出ますよね…。

こちらもbackground.tsにて、下記の様に修正を行います:

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
    // 中略
    frame: false, // フレームを非表示にする(ここを追加)
    // 中略
  });
}

さて、ここまでくるとアプリを終了するには強制終了しか手段がなくなってしまいます…。
おまけにフレームがないので、「デスクトップマスコットを画面上の好きな位置に動かす」なんてことも出来ません。
流石にこれではアプリとしてどうなのよ、と言うことで代替となるボタンを独自で作りましょう。

フレームにあるボタン群の代わりを作成

作成イメージとしてはこんな感じです:

作成イメージ

Vue.jsアプリケーション内部にボタンを配置する形をとります。
今回は「最小化表示」ボタンはいらないと思うので、「アプリの終了ボタン」と「アプリの移動ボタン(アプリを画面上の好きな位置に動かすボタン)」のみ実装します。
まずは「アプリの終了ボタン」から見ていきましょう。

アプリの終了ボタンを自作

アプリの終了ボタンを自作する…とはいえ、Electronが起動してしまえば画面に表示されるのはHTML&JavaScriptの単なるWebアプリケーション…。
「Webアプリケーション内部からアプリ終了等の、Electronの機能って操作できるの?」…とお思いかと思いますが、そこはElectron。バッチリ対応してます。

Electronには「WebアプリケーションからElectronの機能を使える」Electron APIと言うものが提供されています。 www.electronjs.org

できることは様々で、今回やりたい「アプリの終了」機能はもちろん…。
「使用端末の電源状態をモニタする」機能や「システムの環境設定を取得する」機能、更にはMac OS限定ですが「TouchBarにボタンを表示する」なんて機能もあります。
上の公式Docsを見ていただければ、色々と夢が膨らむかと。

まずはElectron APIをVue.jsから叩ける様にします。
いつもの様にbackground.tsにて、下記の様に修正を行いましょう:

  const win = new BrowserWindow({
    // ...
    webPreferences: {
      // ブラウザがNode.jsの機能を使用できる
      nodeIntegration: true,
    },
    // ...
  })

ちなみに、ここの設定値をONにすると「WebアプリケーションからNode.jsが触れる様になるため」「ElectronでXSS攻撃を受けた場合、ユーザのPCのデータも操作されてしまう危険」が発生します。参考
今回の様な外部通信を一切行わないデスクトップマスコットであれば問題ないかと思いますが、そうでない場合は十分ご注意を。
※もしかしたら、こうしなくてもElectron APIは使えるかもしれません…筆者はこれをやらないとボタンが実装できなかったので適用しました

また、先の章で「画面上のクリックを無効化」してしまったので…。
このままではボタンを作ってもクリックができず詰んでしまうため、そちらの対応も行います。

background.tsにて先の章で設定した文を、下記の様に書き換えます:

  // マウスイベントを無視
  // mouseenterやmouseleaveといったイベントを検知できるようにするため、`forward`オプションを追加
  mainWindow.setIgnoreMouseEvents(true, { forward: true });

こうすることで、Webアプリケーション側の各DOM要素は「クリックはできないがマウスがその要素上に乗った時or離れた時がわかる」様になるので…。
Vue.jsの場合では下記の様にコードを書けば、「そのDOM要素しかクリックを受け付けない」ボタンが作れます。

<template>
  <button
    v-on:mouseenter="onMouseEnter"
    v-on:mouseleave="onMouseLeave"
  ></button>
</template>

<script lang="ts">
import { Component, Prop, Watch, Vue } from "vue-property-decorator";
declare global {
  interface Window {
    require: any;
  }
}
const electron = window.require("electron");
/**
 * Electronのアプリ終了ボタンを提供するコンポーネント。
 */
@Component({
  components: {},
})
export default class ExitButton extends Vue {
  private onMouseEnter() {
    // 要素にマウスポインタが乗っている間、マウスイベントの無視をやめる
    electron.remote.getCurrentWindow().setIgnoreMouseEvents(false);
  }
  private onMouseLeave() {
    // 要素からマウスが離れたら、マウスイベントを無視する
    electron.remote
      .getCurrentWindow()
      .setIgnoreMouseEvents(true, { forward: true });
  }
}
</script>

あとは上記を元にボタンを作成し…。
クリック時にelectron.remote.getCurrentWindow().close()を発火させる様にすればOKです。

参考に、筆者が今回作った「アプリの終了ボタン」のソースコードを置いておきます。参考になれば…。
github.com

アプリの移動ボタンを自作

「アプリの移動」機能については、実はElectron APIに頼らずとも実装できます。
「押下するとアプリを移動させたい」DOM要素に対し、-webkit-app-region: dragCSSとしてくっつければOKです。トテモカンタン!

ただ、クリック無効化は回避させないといけないため…。
「アプリの終了ボタン」を作った時と同じ容量で、クリックを受け付けるボタンを作りましょう。

…しかしここで問題が。
なんと、Windows環境では-webkit-app-region: dragが効かないのか、アプリの移動ができず…。
色々調べてもお手上げ状態だったので、筆者は今回最終手段として「Windowsではアプリの移動ボタンをなくす」様にしました。無念…。

こちらも参考に、筆者が今回作った「アプリの終了ボタン」のソースコードを置いておきます。参考になれば…。
github.com

用意したアニメーション画像を画面上に出す

お次はアニメーション画像を使用し、画面上にかわいいあのキャラたちを表示させていきましょう。
筆者は今回アニメーションを作る上で、下記の様に作成しました:

1番目は技術関連の話じゃないのでパス、というわけで2番目の「VSCode拡張機能「Image Sprites」を使用し画像をスプライト画像化」を見ていきます。

この拡張機能は「フォルダを指定すると、そのフォルダ内の画像をスプライト画像化(1枚の大きな画像にまとめる)」してくれ神ツールです。
どう言うふうに動作をするかは、GithubのデモGIF動画を見たらなんとなくわかるんじゃないかと。

さらになんと、下記の様なスプライト画像1つ1つを表示可能なCSSまで出力してくれます…!
これを神ツールと言わずなんと言いましょう…。

/*
   Generated by Image Sprites (https://marketplace.visualstudio.com/items?itemName=gurayyarar.imagesprites)
   Author: Güray Yarar (https://github.com/gurayyarar)
*/

.potato01 {
  background-image: url("potato01.sprite.png?v=v9knS6b7oFlTVVFfN2TCTWPPOkhSIragttCQa");
  background-repeat: no-repeat;
  display: inline-block;
}

.potato01.1 {
  width: 584px;
  height: 356px;
  background-position: -5px -5px;
}
.potato01.1a {
  width: 584px;
  height: 356px;
  background-position: -5px -366px;
}

と言うわけで、これをもとにスプライト画像&CSS作成をすれば後は表示させるだけで終わりです。
ちなみに完成させたものがこちら:

完成品プレビュー

だばだば走ってるポテトが両端に行くと急に消えてしまう風に見えるのを防ぐため、下記を参考に両端にグラデーションを入れてます。
mask-imageで両端に透過グラデーションのマスクをかける - Qiita

アイコンを作成&アプリをパッケージング

ここまで来たら、後はアプリをパッケージング(アプリケーションとして出力)するだけです。
…ただ、Electronは何も設定しないと出力されたアプリのアイコンは「Electronアイコン」になります。

何も設定していない時のElectronアイコン

折角作ったのにこれでは味気ないので、アプリ用のアイコンも作っちゃいましょう。
アプリ用のアイコンは「512px × 512px」サイズのPNGファイルでOKなので、サクッと用意しちゃいます。
Windows専用であれば「256px × 256px」でもOKです

後はアイコン設定をvue.config.jsに書けばOKです!

module.exports = {
  // transpileDependencies: ["vuetify"],
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        mac: {
          icon: "src/assets/app.png",
        },
        win: {
          icon: "src/assets/app.png",
        },
      },
    },
  },
};

早速ビルドして確認しましょう…。

独自アイコンを定義した後のElectronアイコン

無事にアイコンが独自のものになりました!
独自アイコンにすると、一気に自分で作った感が出ますよね…。

独自アイコンを適用した時のデスクトップアイコン
デスクトップアイコンにも独自アイコンが適用されます

ともあれ、これでデスクトップマスコット作成は完了です!
後はWindows or Mac OS用にそれぞれアプリをパッケージングするなり、配布用ドキュメントなりを整えてあげれば完璧です。

おわりに

今回は「Electronでデスクトップマスコットを作成した経緯、使用技術」について見ていきました。
自作アプリケーションなんて初めて作ったので、配布用の準備をしてる時は非常に胸が躍りましたね…。

今回作成したデスクトップマスコットはソースコードも含め、Githubにて公開中です。
github.com

Vue.jsだけでなくTypeScriptを採用しているなど、本記事には記載していない点もありますが…何かの参考になれば幸いです。

今後はこの知見を生かし、Electron×Live2dでデスクトップマスコットが作れればと思います。
…が、Live2dのJavaScriptSDKに関する参考記事が少なく…本当にやれるかは不安です。
もしできたらまた記事化しておきたいなと思います。

それでは|д゚*)