【個人開発】ミャクミャク様っぽい何かを描ける何かを作った
口の部品がないけど勘弁です( ・ω・)
量産型 ミャクミャク様
大きさはある程度のパターンからランダムで配置します。押すボタンによって、キャンバス上に出現する図形が変わります。
- 『あかいの』…赤い正円
- 『あおいの』…青い正円
- 『めだま』…目玉の正円
- 『あかーいの』…赤い楕円
- 『あおーいの』…青い楕円
- 『めだーま』…目玉の楕円
定数として定義されているパターンからランダムで決定します。欲しい部品がある場合は配置されるまで粘る必要があります。パターン定義は以下なので、『黄色にしたい』『他の大きさが欲しい』などある場合は、ローカル環境で編集しながら動かすのが手っ取り早いでしょう。
export class Constants { /** * まるいミャクの大きさ * X半径(px) = Y半径(px) */ static RADIUSES = [20, 30, 40, 50]; /** * ミャクミャク様の赤 */ static MYAKU_RED = "rgb(229, 0, 18)"; }
ただし編集するたびにリロードされるので、そこまでするなら後述する Myaku クラスを直接使った方が早い気もします汗。
以下は図形設置以外の特殊なボタンです。
サイズ調整はデバイスによっては微妙だと思います。ぶっちゃけ大体で作った感があるので、多めに見てください状態です。まあ趣味の範囲で作ってますので「ほならね?( ´∀`)」ですわ。
ちなみに図形の上で右クリックすると削除される隠し機能?があります。どこかに説明を書こうかとも思ったんですが、どこに書くか別ページにするかREADMEに誘導するか、迷い初めた辺りで考えるのをやめました。
「これ、ミャクミャク様というより、いのちの輝きくんジェネレーターじゃね?」と思った読者諸氏は聡明である( ・ω・)
何故作ろうと思ったのですか?
これに関しては今でも分かりません。
特にミャクミャク様が大好きということもない。Twitterでちょくちょく見かける有名キャラクター程度の存在。話題性は抜群だが、私にはその人気にあやかれるほどのフォロワーや知名度がある訳でもない。
ミャクミャク様を作るくらいなら、他にもつぶやいてきた未実装のあれやそれがあるだろうと。ひょっとするとこれが『プライミング効果』というやつでしょうか(10回クイズのように前情報に引っ張られる効果)。
ただ作り始めたら割と最後までできてしまったというのは大きいかもしれないです。大体途中で飽きるか面倒になってきて放置が多いので。複雑な形でないのも良かったと思います。これが最初から福笑いみたいなのを作っていたら、恐らく途中で挫折していたことでしょう。
由来はなんですか?
こういうジェネレーターを使うと、沢山の人が各々ミャクミャク様を作ることになるので。
何となく『Mass Production Evangelion』『Mass Production ほにゃらら Gundam』とか呼ぶのを文字って、『Mass Production Myaku-Myaku』にしました。ミャクミャク様って英語でなんて書くんだろうと思って調べたら、そのまんまでした。
サイトの説明文は『あかいまる・あおいまる・めだまのついたまる を ならべて、じぶん だけ の みゃくみゃくさま を つくろう!』。何となく全部ひらがなで統一してあります。お子様と日本語初心者の方に優しい仕様になっております。
単にボタンの名前を考えるのが面倒だっただけでもあります。この記事を書いてから常に面倒臭がっている気がしますが、きっと気の所為でしょう。
今後の予定は?
今のところ予定なしです。
少しマシになったとはいえ、EyeMyaku クラスの設計は未だに納得いってないので、手直し程度はするかもしれません。
みたいな機能は考えて、おおよそ実装の見当はついていますが、やはり手を動かすのと何かあったときの対処もやはり面倒で。だったら他のものを作るんでないかと思います。
開発中のあれこれ
環境構築からデプロイまで
Node.js だけで完結しています。package.json を見たら、それっぽいコマンドが並んでいるので迷わないかと。設定不要を謳う Web アプリケーションバンドラ Parcel を利用、詳細は公式サイトをご覧ください。
GitHub Actions では、以下の処理を行っています。.github/workflows/main.yml を見たら分かると思いますが(この辺り本当に解説することがない…)。
- Prettier & ESLint & Stylelint によるチェック
- Cypress による E2Eテスト
- cypress-io/github-action@v4
- Parcel でビルド
- GitHub Pages にデプロイ
- JamesIves/github-pages-deploy-action@v4
有名な Action なので説明は割愛。
データベースやWebサーバーは小まめに面倒を見ないといけないので大変ですが、静的ファイルをビルドして置いておくだけ系は比較的管理が楽なので、ちょいちょい作る気になります。お手軽さは大事。
dist と out を分けたのは確か E2Eテストで npm run serve していて、そのまま dist を使用すると生成物がおかしくなるからだったような(うろ覚え)。
自動テストの方針
このアプリはキャンバスに描かれる画と、ダウンロードされる画像が全てなので、思い切ってCypress による E2Eテスト一本にしています。
noranuko13/react-tutorial のように各々のコンポーネントでテストすることも考えたんですが、最終的な出力先が canvas でランダム要素もあるので、どこまでもテストが書きにくい…誰だこんな設計にしたのは。
コストが高いのは重々承知。仮にクラスごとファイルごとにテストを用意したとしても、今度はクラス設計を見直すときに面倒臭がりそうで。ふわふわしている今の段階で作り込むのは停滞しか産まない気がしまして。
要は結果さえ担保できていればいいんだよ(暴論)。失敬、塩梅の良い方法を考え中です。
自動テストの詰まりポイント
テスト周辺の実装は、地味に面倒ポイントが多かった記憶があります。それでも他のツールより立ち上げも設定も楽なんですけれど。テストしたいことが特殊過ぎるのが原因。
キャンバス上の図形をドラッグ&ドロップ
例えばキャンバス上の図形をドラッグ&ドロップするところ。範囲外にはみ出させる、というのをどう実現するのだろうか。調べたものの分からず終いで、苦肉の策として擬似的に pointerout イベントをトリガーしています。
it("キャンバスの描画領域外にはみ出さないこと", () => { cy.get("body").trigger("pointerdown", 360, 50); cy.get("body").trigger("pointermove", 360, 350); cy.get("body").trigger("pointerout", 360, 350); // 擬似再現 cy.get("body").trigger("pointermove", 360, 400); cy.get("body").trigger("pointerup", 360, 400); });
画像差分のテスト
あるいは生成画像の差分を取ってテストの成否を確認するところ。予想される画像と実際の画像が一致しているか確認します。pngjs と pixelmatch を使用して、Cypress.Tasks に comparePng というメソッドを定義。妥協のN秒待ちをしているので、正直まだどうにかできる気がします。
- mpmm/cypress/support/tasks.ts at 18bac946573d9fc5ad43c97dd98e563f4382c521 · noranuko13/mpmm · GitHub
- コードの量が多いので、興味のある方は直接GitHubを見た方いいと思うよ。
ブラウザ間差異?
画像比較でもう 1 つの詰まりポイント、cypress open, cypress run, cypress run --browser chrome で微妙に差分が合わない…当時の私がつぶやいているのですが、結局 CI とローカル環境で実行するテストのコマンドを合わせています。Cypress はキャプチャや動画を保存してくれるので、それでも困らないんですが何かもやもやします。
画像フォルダはクリアされてるし、nodeのversionも合わせたし。
— 野良ぬこ@副業フリーランス (@noranyanko13) October 17, 2022
ローカルでcypress open, cypress runしても成功する。
これもローカルでcypress run --browser chromeすると確実にズレてテストが落ちる。
差分を見ると確かに円の外周で差分が検出されている…謎。
待って、自分で解きます。 pic.twitter.com/zVr8IegchT
Cypress.Commands or Cypress.Tasks ?
Cypress.Commands, Cypress.Tasks も、最初のうちはどちらに何を書くか随分迷っていましたね。TypeScript だからいちいち書き方を調べないといけないし、ESLint と WebStorm が警告してくるし。
// eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-unused-vars declare namespace Cypress { // noinspection JSUnusedGlobalSymbols interface Chainable {
テスト専用ページを作成
あとは仕組み的に当然なんですが、ミャクミャク様を構成する部品(プロジェクト内ではミャクと呼称)のテストをするために、テスト用ページを作らざるを得なかったこと。serve コマンドでは http://localhost:1234/_ でアクセスできるようになっていますが、build では生成されないようになっています。
"scripts": { "serve": "rimraf dist && parcel src/index.html src/_/index.html", "build": "rimraf out && parcel build src/index.html --no-source-maps --public-url https://noranuko13.github.io/mpmm --dist-dir out" },
こんな感じで方眼紙を書いた後に、ミャクミャク様になる何かを整列させています。これで Myaku クラス周りをどう修正しても、テストさえ通ればご安心です。決して星のカービィに出てくるあのキャラではないです。
テスト用ミャクの隠しコマンド
唯一、本番に影響しているのが、マウスイベントテスト用のミャク配置隠しコマンド。ゲームの隠しコマンドってこういう風に生まれるんですかね? 超ランダムな文字列なので意図的に入力するのは大変でしょうが、黒と茶色の丸が出てくるのは、今のところこれだけです。
全く同じものをテスト用のページに実装しても、いつ剥離するか分かったもんでないですし。特にやばいデータが出てくるもんでもないですし。
window.addEventListener("load", () => { /** * テスト用の隠しコマンド */ const keys: string[] = []; document.onkeydown = (event: KeyboardEvent) => { const COMMAND = "4b2fb724c6e49b"; keys.push(event.key); if (keys.join("") === COMMAND) { canvas.putOnMyaku(new Myaku(new ShapeParams(new Coord(50, 50), 40, 40), new StyleParams("rgb(115, 103, 7)"))); canvas.putOnMyaku(new Myaku(new ShapeParams(new Coord(90, 50), 40, 40), new StyleParams("rgb(34, 36, 38)"))); canvas.redraw(); } if (COMMAND.length < keys.length) { keys.splice(0); } }; });
やはりテスト周りは何かしら詰まりポイントがある前提で対応するのがいいでしょうね。
コードフォーマッター・静的解析ツールなど
Prettier & ESLint & Stylelint 、鉄板。
普段なら Prettier は使わないんですが、今回はきちんと導入してますね、当時の私。恐らく他の開発者が触る構想があったのだと思います。設定が七面倒臭いのと、個人開発ならどのみち触るのは私だけ、微妙に気に食わない整形もあったりするので、サボることが多いです。
仕事で権限があれば確実に導入します。チーム開発で各々の好みや哲学でぶつかり合うのは不毛なので、この場合はむしろ書き方が決まってる方が無用な争いを避けられると思います。話が逸れ始めたので、この辺で。
プロトタイプの話
量産型ミャクミャク様には、零号機とmjs版がありました。いや、この場合量産型ミャクミャク様のプロトタイプであって、量産されたミャクミャク様の機体のことでは…こ、細かいことはいいんですよ汗。
- branch: v0-unit-00
- branch: v1-mjs-build
確か当初は *.js, *.mjs などを使って初心者が難しい操作をせずとも触れるように…的なことを考えていたのですが、「結局サーバーは起動しないといけないのかいっ!」となってからは、難しいことをせずに Parcel & TypeScript を素直に使う感じにした記憶。
こういう実験的な試行錯誤はあまり見かけないですが、開発者視点で貴重な情報ですし、残しておいてもいいかなと。
数学
今回、最大にして最高の難所。プログラミングに数学が必要か私には判断付きませんが、少なくともキャンバス上のミャクミャク様を移動するのに数学は必要です。当たり判定というやつです。
どなたかに監修を受けている訳でもないので、結果的に合っているが方法は間違っているかもしれません。少なくとも零号機の時点では適当で、テストを書いている最中に修正をしてた筈。
- mpmm/src/scripts/sketch/shape/ellipse-shape.ts at 18bac946573d9fc5ad43c97dd98e563f4382c521 · noranuko13/mpmm · GitHub
(そして whiteAngle の修正漏れを今見つける…(ヽ´ω`)アアァ)
楕円の場合は =1 であれば完全に一致ですが、ミャクを掴むのは人間なので 1.2 以下までは掴めるようになっています。
数学と書きましたが、実際に思い出したのは地学?天文学?の惑星の楕円軌道の計算の方で。誠に申し訳ないのですがやはり実利というか、具体的にイメージできた方が人間は覚えがいいらしいです。
ミャクミャク様の口は恐らく楕円を 2 個重ねて、各々当たり判定をすればいいと思うんですけど(canvas に図形を書くとき透明な方をどうしたらいいんだろうという謎は残る。canvas 2 個でレイヤーを…とかなってくると、正直かなり面倒でございます)。
クラス設計
少し前まで Myaku, EyeMyaku の実装が気に食わず、試行錯誤と散歩と思案にふけること数週間、最終的に今の実装に落ち着きました。
当たり判定や座標移動は baseShape (一番外側の図形)、これが変わることは基本ないので継承のような気がしなくもないんですが。私の力量では最終的にミャクミャク様が本当に神様クラスになってしまいそうなので。
あと久々にイテレーターを使いました。キャンバス上でミャクを掴む・消すときの判定は後に格納したミャクから、描画するときの判定は先に格納したミャクからなので、どちらからも取り出せるように切り替えを行っています。
あと Gesso (ジェッソ)はキャンバスに塗る下地材、 Fabric は布です。別にお洒落にしたいからではなくて、似たような単語を別の箇所に使いたくないので、故意に重複しない同義語に置き換えることはよくします。clear は『くりあ』ボタンで既に使ってますし、何度もでてくるとややこしいですから。
main.ts が完全にルーティングみたいになってるんですよね。canvas をデータベースと見做して、コントローラーを作れば完全に MVC 、さしずめ event はリクエストというところですか。いや正直これ以上分けても旨味がないので、これはこれでいいと思うんですけど。
デザイン
CSS はシンプル・イズ・ベストで、あえて余計な装飾を付けないようにしています。canvas を触るとき余計な装飾が付いていると、要素の大きさや背景の指定によっては迷子になりやすいと思ったので。
色や余白は雑な方の適当です。赤・青は決まっていますが、他はどうでもいいので何となく黄色をチョイス。その他のボタンは白黒でいいかと。次に追加するとしたら…多分緑とかになると思いますが、これ以上ボタンを追加すると初期表示の縦60%との相談にもなるので、難しいところですね。
これを書いていて思い出したんですけど。プログラミングが当たり前になった未来の子どもの夏休みの自主制作って感じのアプリですね。たまにはこういうのもいいと思います。
ライセンス
ミャクミャク様は二次創作のガイドラインが出ていますので、 公式キャラクターについて|公益社団法人2025年日本国際博覧会協会 を参照ください。
私が書いたコードの部分は MIT License なので、自己責任でご自由に。
力尽きたのでこの辺でお暇させていただきます。最後まで読んでいただきありがとうございました。