ハイハイスクールアドベンチャー_web版
差分
このページの2つのバージョン間の差分を表示します。
| 両方とも前のリビジョン前のリビジョン次のリビジョン | 前のリビジョン | ||
| ハイハイスクールアドベンチャー_web版 [2024/02/14 09:34] – [概要] araki | ハイハイスクールアドベンチャー_web版 [2025/10/20 02:00] (現在) – [PWA] araki | ||
|---|---|---|---|
| 行 66: | 行 66: | ||
| じゃあ、メモリ上に置いといて、書き込みは処理が終わってまとめてやればいいんじゃないかということになるが、画像を RGBA形式で保持していると、結局読み出しが遅いのだ。 | じゃあ、メモリ上に置いといて、書き込みは処理が終わってまとめてやればいいんじゃないかということになるが、画像を RGBA形式で保持していると、結局読み出しが遅いのだ。 | ||
| - | そもそもそんなに色数使わないので、8bitのインデックスを作ってペイントバッファとしてそれを使えばいいんじゃないかということに気づいてそのようにした。 | + | そもそもそんなに色数使わないので、8bitのインデックスを作ってペイントバッファとしてそれを使えばいいんじゃないかということに気づいて((気づきが遅い!))そのようにした。 |
| なお、当初は、普通に一枚の canvasでやりくりしてたし、速度的には十分だったが、下に書くように、画面の表示領域をネイティブのサイズより大きくしたかったので、画面の canvas要素へ拡大転送するための canvasを作成して、ネイティブサイズ((256x152ピクセル))の画像はそこに描画されている。 | なお、当初は、普通に一枚の canvasでやりくりしてたし、速度的には十分だったが、下に書くように、画面の表示領域をネイティブのサイズより大きくしたかったので、画面の canvas要素へ拡大転送するための canvasを作成して、ネイティブサイズ((256x152ピクセル))の画像はそこに描画されている。 | ||
| 行 79: | 行 79: | ||
| ウェブページ上に 512x304((要するに倍のサイズ))で canvasを用意して、プログラム中ではネイティブサイズのダミーの canvas に書き込みを行い、倍サイズの表示領域に拡大転送することでこれを実現している。 | ウェブページ上に 512x304((要するに倍のサイズ))で canvasを用意して、プログラム中ではネイティブサイズのダミーの canvas に書き込みを行い、倍サイズの表示領域に拡大転送することでこれを実現している。 | ||
| - | < | + | < |
| this.zctx = document.getElementById(" | this.zctx = document.getElementById(" | ||
| this.zctx.imageSmoothingEnabled = true; | this.zctx.imageSmoothingEnabled = true; | ||
| 行 100: | 行 100: | ||
| 2011年ごろの JavaScript によるオブジェクト指向プログラミングというと、クラスも何もなくて、普通の変数の宣言を利用してやっていたのが実態であった。 | 2011年ごろの JavaScript によるオブジェクト指向プログラミングというと、クラスも何もなくて、普通の変数の宣言を利用してやっていたのが実態であった。 | ||
| - | < | + | < |
| var Klass = function() | var Klass = function() | ||
| { | { | ||
| 行 122: | 行 122: | ||
| 基本的にデータファイルも、CGIで描画させた画像もこの枠組みを使ってやりとりしていたので、これも大きかった。 | 基本的にデータファイルも、CGIで描画させた画像もこの枠組みを使ってやりとりしていたので、これも大きかった。 | ||
| - | < | + | < |
| var req = new XMLHttpRequest(); | var req = new XMLHttpRequest(); | ||
| req.open(' | req.open(' | ||
| 行 136: | 行 136: | ||
| これはラムダ式を使うのが今風なんだと。 | これはラムダ式を使うのが今風なんだと。 | ||
| - | < | + | < |
| fetch(' | fetch(' | ||
| var buf = new Uint8Array(abuf); | var buf = new Uint8Array(abuf); | ||
| 行 148: | 行 148: | ||
| 全部をひとまとまりでやろうとするとうまく動かない。 | 全部をひとまとまりでやろうとするとうまく動かない。 | ||
| - | < | + | < |
| fetch(' | fetch(' | ||
| if (res.ok) | if (res.ok) | ||
| 行 205: | 行 205: | ||
| マニフェストファイルは、HTMLファイルの linkタグを使って次のようにマッピングする。 | マニフェストファイルは、HTMLファイルの linkタグを使って次のようにマッピングする。 | ||
| - | < | + | < |
| <link rel=" | <link rel=" | ||
| </ | </ | ||
| 行 212: | 行 212: | ||
| もっと詳細な仕様を記述できるが、詳しくは[[https:// | もっと詳細な仕様を記述できるが、詳しくは[[https:// | ||
| - | < | + | < |
| { | { | ||
| " | " | ||
| 行 246: | 行 246: | ||
| Service WorkerもHTMLファイルから登録してやる必要がある。 | Service WorkerもHTMLファイルから登録してやる必要がある。 | ||
| - | < | + | < |
| if (' | if (' | ||
| { | { | ||
| - | navigator.serviceWorker.register(' | + | |
| + | if (event.data && event.data.type == ' | ||
| + | const shouldReload = confirm(' | ||
| + | if (shouldReload) | ||
| + | { | ||
| + | window.location.reload(); | ||
| + | } | ||
| + | } | ||
| + | }); | ||
| + | | ||
| } | } | ||
| </ | </ | ||
| 行 255: | 行 264: | ||
| Service Worker本体((ここでは js/ | Service Worker本体((ここでは js/ | ||
| - | < | + | < |
| // cache | // cache | ||
| var CACHE_NAME = ' | var CACHE_NAME = ' | ||
| 行 279: | 行 288: | ||
| ' | ' | ||
| ' | ' | ||
| - | 'js/sw.js' | + | ' |
| ]; | ]; | ||
| 行 298: | 行 307: | ||
| }) | }) | ||
| ); | ); | ||
| + | }); | ||
| + | |||
| + | // update | ||
| + | self.addEventListener(' | ||
| + | const cacheWhitelist = []; // 現在有効なキャッシュ名 | ||
| + | |||
| + | event.waitUntil( | ||
| + | caches.keys().then((cacheNames) => { | ||
| + | return Promise.all( | ||
| + | cacheNames.map((cacheName) => { | ||
| + | // 現在のバージョンに含まれていない(古い)キャッシュを削除 | ||
| + | if (cacheWhitelist.indexOf(cacheName) === -1) { | ||
| + | console.log(' | ||
| + | return caches.delete(cacheName); | ||
| + | } | ||
| + | }) | ||
| + | ); | ||
| + | }) | ||
| + | // すべてのキャッシュ削除が完了したら、クライアントへの制御を引き継ぐ | ||
| + | .then(() => self.clients.claim()) | ||
| + | ); | ||
| + | | ||
| + | event.waitUntil( | ||
| + | // すべてのクライアントに更新完了を通知 | ||
| + | self.clients.claim().then(() => { | ||
| + | self.clients.matchAll().then((clients) => { | ||
| + | clients.forEach(client => { | ||
| + | client.postMessage({ type: ' | ||
| + | }); | ||
| + | }); | ||
| + | }) | ||
| + | ); | ||
| }); | }); | ||
| </ | </ | ||
| 行 303: | 行 344: | ||
| 要はキャッシュを登録しているだけである。 | 要はキャッシュを登録しているだけである。 | ||
| これでオフラインでも使えるようになるらしい((そのように作られていれば))が、やってみていないのでわからない。 | これでオフラインでも使えるようになるらしい((そのように作られていれば))が、やってみていないのでわからない。 | ||
| + | |||
| + | なお、キャッシュされるオブジェクトの指定は、sw.jsのある場所からの相対パスまたは絶対パス、URIなどで、これを間違うとエラーをはいてキャッシュに失敗する。 | ||
| + | |||
| + | ブラウザによってはsw.jsがエラーを出しているとインストールボタンを表示してくれない厳格なやつもいたりするので、よく確認してほしい。 | ||
| === トラブルシュート === | === トラブルシュート === | ||
| 行 310: | 行 355: | ||
| {{:: | {{:: | ||
| + | |||
| + | ==== 音とか ==== | ||
| + | |||
| + | PalmOS、Android、そして Qtでは音がある場面では音が鳴るようになっている。 | ||
| + | Web版を再公開したときに、「誰か解けたのか?」というようなことをつぶやくと「校歌は歌った」というような返事があった。 | ||
| + | そういえばWeb版では音のことをすっかり失念していた。 | ||
| + | |||
| + | HTML5ではaudioコンポーネントがあり、JavaScriptに Audioクラスが実装されている。 | ||
| + | そしてほとんどすべてのモダンなブラウザでMP3形式の音源を鳴らすことができる。 | ||
| + | |||
| + | === 音を鳴らせ === | ||
| + | |||
| + | HTMLで audio 要素を配置してもいいのだが、別に画面上に何かがいる必要はないので、適宜動的に確保する方向で実装する。 | ||
| + | 動的に確保された Audio のインスタンスは再生が終わったらガベージコレクションの対象になるって書いてあるので、音を鳴らすときに確保して、鳴らしたら勝手に消えることを期待する。 | ||
| + | |||
| + | <code javascript> | ||
| + | var m = new Audio(); | ||
| + | m.src = this.base_uri + "/ | ||
| + | m.load(); | ||
| + | m.volume = 1.0; | ||
| + | m.loop = false; | ||
| + | m.play(); | ||
| + | </ | ||
| + | |||
| + | ボリュームは1.0が100%になる。 | ||
| + | |||
| + | === 音を鳴らすな === | ||
| + | |||
| + | 多分、音鳴ったら困る環境の人も少なくないだろう。 | ||
| + | いきなり、ハイハイスクールの校歌が流れたらびっくりするだろうし、大体が、この校歌結構長尺で鳴り出すと止まらないのだ。 | ||
| + | |||
| + | なので、ミュート機能はつけておいた方がいいと判断した。 | ||
| + | まあ、PCなり、スマホなりでミュートできるだろうけれど。 | ||
| + | どっちかというと、ミュートというよりデフォルトでミュートしておいて、鳴らしたい人だけ解除するイメージだ。 | ||
| + | |||
| + | まあ、単純にチェックボックスを一個置いておけばいいのだが、今時のインターフェイスとしてはちょっとカッコ悪い。 | ||
| + | そもそもこの昭和レトロなアドベンチャーゲームに何をいっているんだという向きもあるだろうが、せっかくなので、今風のスライドスイッチにしておきたい。 | ||
| + | |||
| + | Safariなら、そういう要素がサポートされているらしいが、多くのブラウザでは未サポート((2024年2月現在))なので、別の方法でやることにする。 | ||
| + | |||
| + | 世の中には優れた人が多くいて、既にCSSを駆使して、そういうものをチェックボックスに付加してくれているので、まるっとそれをいただくことにする。 | ||
| + | |||
| + | <code html> | ||
| + | <label for=" | ||
| + | <div class=" | ||
| + | <input type=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <span class=" | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | チェックボックスがスイッチの本体だが、チェックボックスを消して、代わりに circleと baseでスイッチを構成する。 | ||
| + | この部分はすべてCSSに記述されている。 | ||
| + | |||
| + | <code css> | ||
| + | .switch { | ||
| + | position: relative; | ||
| + | } | ||
| + | |||
| + | .switch_label { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | } | ||
| + | |||
| + | .switch input[type=' | ||
| + | position: absolute; | ||
| + | width: 0; | ||
| + | height: 0; | ||
| + | } | ||
| + | |||
| + | .switch .base { | ||
| + | width: 28px; | ||
| + | border-radius: | ||
| + | height: 16px; | ||
| + | background-color: | ||
| + | } | ||
| + | |||
| + | .switch .circle { | ||
| + | position: absolute; | ||
| + | top: 2px; | ||
| + | left: 2px; | ||
| + | width: 12px; | ||
| + | height: 12px; | ||
| + | border-radius: | ||
| + | background-color: | ||
| + | transition: | ||
| + | } | ||
| + | |||
| + | .title { | ||
| + | margin-left: | ||
| + | } | ||
| + | |||
| + | .switch input: | ||
| + | background-color: | ||
| + | transition: | ||
| + | } | ||
| + | |||
| + | .switch input: | ||
| + | transform: translateX(100%); | ||
| + | background-color: | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | なお、実際に使用しているスタイルシートは、レスポンシブデザインっぽく、px使わないでサイズを指定しています。 | ||
| + | |||
| + | ==== 設定 ==== | ||
| + | |||
| + | 基本的には、特に設定項目はなかったのだが、音を鳴らすことにしたら、Qt版同様に、音を鳴らすのかどうかを覚えておいた方がいいような気がしてきた。 | ||
| + | |||
| + | まあ、毎回 Mute ONにしてしまうでもいいような気もしたが、将来何かの設定を追加したときに、それも覚えておいた方がいいような気がしたので、保存できるようにした。 | ||
| + | |||
| + | ゲームデータ保存用objectStoreはIndexedDBに作ってあったので、バージョンを2にして、設定用のobjectStoreも作るようにした。 | ||
| + | DBのアップグレードとかしないと思ってたが、いきなりすることになるとは先見の明がなさ過ぎた。 | ||
| + | |||
| + | <code javascript> | ||
| + | const req = indexedDB.open(Database.DBNAME, | ||
| + | req.onupgradeneeded = (event) => | ||
| + | { | ||
| + | const db = event.target.result; | ||
| + | const oldver = event.oldVersion; | ||
| + | const newver = event.newVersion; | ||
| + | const migrations = { | ||
| + | " | ||
| + | db.createObjectStore(Database.STORENAME, | ||
| + | }, | ||
| + | " | ||
| + | db.createObjectStore(Database.PREFSTORE, | ||
| + | } | ||
| + | } | ||
| + | for (var v = oldver + 1 ; v <= newver ; v++) | ||
| + | { | ||
| + | if (migrations[v]) migrations[v](); | ||
| + | } | ||
| + | }; | ||
| + | </ | ||
| + | |||
| + | バージョン更新のやり方は、どこかで見かけたのを参考にした。 | ||
| + | |||
| + | ==== テーマ ==== | ||
| + | |||
| + | 今どきのアプリとしては、システムのテーマに追随しないわけにはいかない。 | ||
| + | |||
| + | テーマへの追随に関しては、システムがダークモードだったら、body要素に class=' | ||
| + | これを動的に行うコードを追加し、初期化時にチェック、またシステム設定が変更された場合には検出し追随するようにイベントリスナーを登録して対応する。 | ||
| + | |||
| + | <code javascript> | ||
| + | applySystemTheme(e) | ||
| + | { | ||
| + | if (e.matches) | ||
| + | { | ||
| + | document.body.classList.add(' | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | document.body.classList.remove(' | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | this._darkModeQuery = window.matchMedia(' | ||
| + | this.applySystemTheme(this._darkModeQuery); | ||
| + | this._darkModeQuery.addListener(this.applySystemTheme); | ||
| + | </ | ||
| + | |||
ハイハイスクールアドベンチャー_web版.1707903291.txt.gz · 最終更新: by araki
