この記事はクソアプリ Advent Calendar 2020の7日目です。昨日は@fujit33さんの【待ってました】ラーメンにコショウをかけるためのアプリを作りました!でした ... オチまで秀逸
作ったおすし
リモートワーク中のちょっとした待ち時間、みなさんどうしてますか?
ダウンロードが遅い時・ビルドが長い時・イラレがクラッシュした時...
「ちょっと手持ち無沙汰だけどTwitter見るほどでも(仕事しろ)」...そんな時、軽くお寿司つまんだりしたくなりませんか?
なりますよね?この記事を見たあなたもきっと今すぐお寿司をつまみたい気持ちに心が支配されたはず。
そんな時にこのアプリを起動します。
https://github.com/yuneco/osushi-desktop#download
5.良い積みができたらスクショをTwitterに流しましょう(隙あらば宣伝
なぜ作ったか
繁忙期で残業続きの1週間が終わり、ようやく迎えた12月5日土曜日(おととい)...
今日気づいた一番怖いこと報告しますー
— ゆき (@yuneco) December 5, 2020
(完全に忘れてた) pic.twitter.com/0n4u5vZCtT
あと2日wwww もうお寿司食べるしかないじゃないですかwww
技術と解説
ここからは少しだけ真面目に解説。
ソースコード:https://github.com/yuneco/osushi-desktop
使ったフレームワーク・ツールなど
- Electron: webアプリをappやexeにできる魔法使い。...と見せかけてブラウザー(Chromium)丸ごと抱き込んで動かす実は筋肉系。力 is パワー
- Vue.js: webアプリがサクッと作れる偉い子。たまに「でもそれreactの後追いだよね?」って言われて泣く。今回はVue3 + composition-apiです
- matter.js: おすしが落ちたり積まれたりするために必要な物理演算をやってくれる天才。冬場はマシンを温めるのにも最適。
- TypeScript: 理屈っぽくて面倒なTSちゃんでだけど、クソアプリみたいに勢いで書きなぐる時にはむしろ頼りになる
- electron-builder: Vue-CLIで使えるプラグイン。Vueのプロジェクトに一発でElectronを導入してくれる神の遣い。正直何も理解しないでとりあえずコマンド叩いたら使えた
- icon-gen: 配布用のアイコンをまとめて作ってくれる気の利く子
- Adobe Illustrator on iPad: おすしの絵を描くよ!便利だけどバグも多いよ!
Step1: Vueのプロジェクトを作る
Vue-CLIのvue create
を使って、普通に好きな感じの設定で作ります。
ただし、Routerを使いたい場合はHistoryMode使う?
の質問にNoで答えてハッシュモードにする必要あり。多分気をつけるのはそれくらい
Step2: Electronの導入
中身の開発に入る前に、そのままElectronを導入します。
cd 作ったVueプロジェクト
vue add electron-builder
バージョンとか聞かれると思うけど、適当に新しいの選らんどけばOK
終わったら↓でデバッグ起動。見慣れたVueのHelloWorldがElectron
って名前の独立したアプリで表示されるはず。
npm run electron:serve
Step3: ウインドウを透過させる
ここら辺から中身の話。
今回のおすしアプリはウインドウの背景を透過させるのがマストです。まずそこからやってみます。
透過自体は簡単で、プロジェクトのルートに生成されているbackground.ts
ってファイルに設定を追加するだけ。
async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
transparent: true, // ✨追加
frame: false, // ✨追加
resizable: false, // ✨追加
backgroundColor: '#00FFFFFF', // ✨追加
hasShadow: false, // ✨追加
alwaysOnTop: true, // ✨追加
webPreferences: {
enableRemoteModule: true, // ✨追加
nodeIntegration: true
}
})
win.setIgnoreMouseEvents(true, { forward: true });
// ...略
}
これで画面は表示されるけど背景は完全に透過して、かつクリックしてもすり抜けてデスクトップや他のアプリを触れるようになります。
各設定項目の意味は大体名前の通りなんだけど、1箇所hasShadow
だけ注意。これがデフォルトのtrue
のままだとアニメーションの残像が残ってチラツキが出るのでfalse
が良いです。影をつけたい場合はCSS側でやるのが吉。
あ、あとFAQなのでググったら出てくるけど、devtoolが出てると透過が無効になるので、最初にdevtoolを別窓で出すようにしておきましょう。
Step4: 透過させつつコンテンツだけはクリックさせる
で、ここまでは簡単だった。1,2時間でできてこれなら余裕じゃーんって思ってた土曜の午後。
なんだけどこの次がちょっと関門。...全部透過させちゃったから何もクリックできないのwww
何かの設定でできるでしょ 余裕余裕...って思ってたらできないらしい...まじか ...でググった結果、
Electronと透過ウィンドウ - 特定の要素ではマウスイベントを受け取る
どうやら
- クリックしたい要素に
mouseenter
した → Electronのクリック透過を無効に - クリックしたい要素から
mouseleave
した → Electronのクリック透過を再度有効に
しないといけないとのこと。。
せっかくなのでVue3らしくcomposition-apiで実装します。
import { onMounted, onBeforeUnmount, Ref } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Electron = require('electron')
const onErnter = (ev: MouseEvent) => {
ev.preventDefault()
Electron.remote.getCurrentWindow().setIgnoreMouseEvents(false)
}
const onLeave = (ev: MouseEvent) => {
ev.preventDefault()
Electron.remote
.getCurrentWindow()
.setIgnoreMouseEvents(true, { forward: true })
}
const resolveRef = (elRef: Ref) => {
const value = elRef.value
if (!value) {
return null
}
return value as HTMLElement
}
const useClick = (elRef: Ref) => {
onMounted(() => {
const targetDom = resolveRef(elRef)
targetDom?.addEventListener('mouseenter', onErnter)
targetDom?.addEventListener('mouseleave', onLeave)
})
onBeforeUnmount(() => {
const targetDom = resolveRef(elRef)
targetDom?.removeEventListener('mouseenter', onErnter)
targetDom?.removeEventListener('mouseleave', onLeave)
})
}
export default useClick
これを使ってClickable
コンポーネントを作って...
<template>
<div class="ClickableRoot" ref="el">
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import useClick from '../compositions/useClick'
export default defineComponent({
setup() {
const el = ref(null)
useClick(el)
return { el }
}
})
</script>
クリックに反応させたい要素をラップすればOK
<template>
<Clickable>
<button>おせるよ!</button>
</Clickable>
</template>
今回は単純に四角形の当たり判定しかとってないけど、もっと自由な形で正確な当たり判定が必要ならSVG使うしかないかな...と
Step5: おすしを表示する
とりあえずおすしのような何かを表示します。
OK...どう見てもMAGUROですね
Step6. おすしに物理演算を適用
まがりなりにもおすしが出たので、次に物理法則をお寿司に適用します。去年ネタで作った●●WebGL(PIXI.js + glsl)と物理演算(matter.js)で可愛い絵本風タピオカ作ったので解説●●のソースをがっつりコピペして土台を作ります。
今回はCanvas(PixiJS)ではなくHTML(Vue)で画面を表示するので、matter.jsからの更新をうけとって、物理演算世界のおすし(ただの長方形)の座標や角度が変わっていたら、その値をVue側にも反映させます。
/src/components/FlyingSushi.vue#L48
onMounted(() => {
// 表示するおすしの座標
state.sushiPos = new Pos(props.initialPos.x, props.initialPos.y)
// 物理世界におすし(長方形)を投入
props.world?.addRect(
props.initialPos.x,
props.initialPos.y,
90,
35,
// 物理世界で座標が更新された時のコールバック
xyr => {
state.sushiPos = new Pos(xyr.x, xyr.y, r2a(xyr.r))
}
)
})
ここまででおすしを積めるようになりました。
Step7. 寿司レーンをつくる
これも特に解説はいらないのでパス。
SushiRail
コンポーネント(寿司レーン)を作り、その中で一定時間ごとにTurnDish
コンポーネント(皿)を生成して左から右にアニメーションさせます。スクロールアウトしたあたりで適当に削除するのを忘れずに。
Step8. 可愛くする
最後にダミーだったおすしの画像をちゃんとしたものに差し替えます。
今回は10月に正式リリースしたばかりのiPad版Adobe Illustratorを使っておすしを描いて、SVGで書き出します。1
ついでに/src/logics/SushiAssets.tsあたりにすしネタの型定義とかも追加します。
/** すしネタ */
export type SushiNeta =
| 'uni'
| 'toro'
| 'tamago'
| 'salmon'
| 'neko'
| 'maguro'
| 'kohada'
| 'ikura'
| 'ika'
| 'ebi'
/** お皿の色 */
export type DishColor = 'dishAka' | 'dishAo' | 'dishGin' | 'dishKin'
Step9. ビルド
正直リリースしたところでこのクソアプリ使う人もいないと思うんだけど、せっかくなのでビルドもします。
クソアプリといえどもアイコンついて単体アプリの形になるとやっぱり気分が上がりますね。
アイコンの作り方は【自作デスクトップアプリ】Electronにアイコンを設定する方法【Vue.js】を参照。
今回はクソアプリなので署名とかはしません。ちゃんと公開する場合(特にMac)は、署名つけてAppleの公証も通さないと普通のダブルクリックで起動できない2ので注意。
まとめ
- お手軽にデスクトップアプリを作りたい時の選択肢としてElectron + Vueはいい感じ。
- おすしは良いものです
明日(12/8)は @namosuke さんです!