100 DAYS / 100 APPS

1日1個、Webアプリを作って公開する実験と記録

12 / 100公開済み

公開アプリ12
制作時間(記録分)3時間15記録済み 6/12 日
制作日数11

TAGS

公開したアプリ

新しい順

Day 012公開中

海賊船の打ち返し係

敵船が撃つ音を聞いて、飛んでくる砲弾を拍で打ち返す1ボタンのリズムゲーム。曲も効果音も海も船も砲弾も、すべてこのページのコードが組み立てている。画像も音源も1つも読み込まず、3Dはライブラリを使わない自前の透視投影で描いている

  • rhythm-game
  • web-audio
  • 3d
制作メモ
AIに任せた
音の時計を正本にする実装。拍の位置は必ず AudioContext.currentTime から逆算し、音の予約は25msごとに100ms先まで入れる。押した時刻は getOutputTimestamp() で音の時計へ直すので、機器の出力遅れが自動で吸収される/曲と効果音を発振器だけで作った。ニ短調・4小節進行の低音と和音とふし、カウントイン、砲弾・二連弾・カモメの3種類の合図音、打った音、船に当たる音、空を切る音。音源ファイルは1つも使っていない/3Dを自前で書いた。透視投影・霧・面の並べ替え・簡易な陰影と、砲弾(八面体を分割した32面の球)・カモメ・敵船・自分の船首・櫂のモデル。頂点はすべてコードで置いている/譜面を拍だけで持つ設計。発射だけを並べて打点は飛行時間から計算する。飛行時間を変えても書き直しが要らない/判定・拍の変換・譜面の妥当性・投影・補正値の読み取りをユニット40件で固定した。隣り合う打点が判定の届く範囲より離れていることも、実時間に直して検査している/E2E 12件。4状態が実際に通ること、キーボードだけで通せること、外部への通信が0本であること、そして画素が実際に描かれていること/権利の線引きの調査と反映。ゲームのルールや仕組みはアイデアなので著作権では保護されないが、名称・キャラクター・曲・効果音・画面構成という表現は保護される。過去には、既存のリズムゲームを再現するツールが権利者の申し立てでGitHubから削除された例(forkを含めて250件以上)もある。そこで、素材を1つも借りず全部その場で作り、公開物に既存作品の固有名を書かない方針にした/敵船が段階的に壊れる演出。ドンピシャで打ち返した弾が「実際に船へ届いた瞬間」に段が進む。帆が裂ける→上の帆が落ちる→マストが折れる→帆をすべて失う→船体が傾く→大破の7段で、遠い小さなシルエットでも輪郭が変わるように壊す順を決めた/おしいの弾は失速して海へ落とし、ドンピシャの弾だけが船へ届くようにした。判定の差を点数ではなく絵で見せるため/体験評価ハーネスを回した。別セッションの評価者が実際にブラウザで遊んで10項目を採点し、返ってきた改善指示を、実装前にコードと実測で1件ずつ裏取りしてから直した/体験評価ハーネスを3周回した(別セッションの評価者が実際にブラウザで遊んで10項目を採点。13→15→16点で合格)。返ってきた指摘は、実装前に必ずコードと実測で裏を取ってから直した/デモ動画に音を付ける道具(tools/render-demo-audio.mjs)。Playwrightは音を録らないので、アプリと同じ音符データ(伴奏・発射・打点)から同じ波形と包絡線でWAVを組み立て直し、ffmpegで重ねている。映像と音のズレが30ms以内であることを、音の立ち上がりと譜面の時刻を突き合わせて実測した
自分でやった
作るものの指定と、途中での方針転換(海賊船のシミュレーション → 音ゲー)/遊びの型・題材・公開の枠の3点を、提示した選択肢から選ぶ判断/「必要ならネットから商用利用可の素材を拾ってよい。足りない3Dモデルは自作せよ」という素材方針の指定/リサーチを厚くしてよいという指示/「ドンピシャだったら船が崩れていく。打ち返せば打ち返すほど崩れる」という演出の指定/体験評価ハーネスを回すという進め方の指示
失敗と修正
テストが全部通っているのに、画面には何も描かれていなかった。スクショを撮って初めて気づいた。原因は、遊んでいない間は海の画面が display:none で大きさが0になること。起動時に1回だけ測って終わりにしていたので、遊び始めても描画用の大きさが 1×1 のままで、その1画素が引き伸ばされて単色の四角になっていた ResizeObserver で大きさを見張るようにし、0のときは測り直さないようにした。あわせて『画素が実際に描かれていること』を見るE2Eを足した(描画用の大きさが実寸か・単色で塗りつぶされていないか)。状態だけ見るテストは、真っ白な画面を素通りさせる
打ち返した砲弾が跳ね返らず、当てても外してもそのまま自分に向かって飛び続けていた。手応えがまったく無い 物体から打点の番号を引けるようにして、当たった弾は打点から元来た方へ戻すようにした。外した弾はそのまま飛んできて船を通り過ぎる
砲弾が水平線と敵船にぴったり重なって、どこにいるのか読めなかった。打点の高さを目線とほぼ同じ(1.32m/目線1.6m)にしていたため、飛んでいる間ずっと遠くの船と同じ高さにいた 打点を0.85mまで下げ、飛び方を山なりにして空を背負わせた。近くに来るほど水平線から離れて、船首の上に降りてくる
奥行きをまっすぐ割っていたので、飛行時間のほとんどで砲弾が点にしか見えず、最後の数%で急に膨らんだ。見かけの大きさは奥行きの逆数なので、等速で近づけると等速には見えない 同じ割合ずつ近づく形(幾何級数)にして、見かけの大きさが一定の速さで大きくなるようにした
船首が横に広すぎて、海を隠す茶色い箱に見えていた 幅を半分以下にし、甲板と舷(ふなべり)と舳先を分けて、輪郭で船だと分かる形にした
ランクの判定で、から振りを1回するだけで最上位から2段落ちる作りになっていた。拍と関係ない入力は減点でよいが、砲弾を通してしまうことより重く扱うのはおかしい 『通してしまった数』を主に見て、から振りは2回まで大目に見るようにした。ユニットテストを書いていて食い違いに気づいた
E2Eが3件落ちて、原因を実装のせいだと読み違えた。実際には dist/ を配って検査する作りなのに、直したあと再ビルドせずに走らせていたので、テストは古いコードを見ていた ビルドしてから走らせ直した。画面の状態と音の時計を1秒ごとに書き出す小さな検査を先に回して、『短い譜面のはずが本編の31打点になっている』と分かったのが決め手だった
drawObjects の閉じ括弧が1つ落ちていたのに、node --check は「構文OK」と答えた。ページは丸ごと読み込みに失敗していて、その状態でビルドまで通った E2Eが13件中11件落ちて気づいた。ブラウザのコンソールを見て『Unexpected end of input』を確認し、括弧を補った。構文検査を通ったことは、ブラウザで動くことの証明にならない
押した時刻を音の時計へ直す仕組みが、最初の1回だけ「遅れ0ms」を返していた。実測すると2回目以降は33msで安定するので、1発目だけ判定が33msぶんずれる 遅れは物理的に正の値にしかならないので、0以下を返してきたら信用せず outputLatency で補うようにした。体験評価者が『+60ms遅らせて押すと1発目だけ判定が違う』と報告してきたのが発端で、実測で裏を取ってから直した
体験評価1周目が13/20点で不合格。当てたときの返り(船が崩れる・連打が通用しない)は既に強いが、入口と自己把握が落ちていた。①遊べる状態になってから初弾まで0.9秒しかなく、1回も成功しないまま本編が始まる ②早いのか遅いのかを一度も教えない ③21/31当てた回と、でたらめ連打の回が同じ見出し・同じ助言 ④HUDの「のこり NN」が残り秒数に読める(実際は残りの砲弾数) ⑤結果の「もう一度」が1200×943で画面の下端で切れて押せない ①カウントインを4拍から8拍に伸ばし、前半で『練習の1発』を撃つようにした(点にもランクにも入らない)。カウントは画面外の小さな文字ではなく海の上に大きく描く ②判定の真下に「はやい/おそい」を出し、結果に平均のズレと補正の目安値を出す ③ランクを4段から6段にし、助言文も段ごとに変えた ④「砲弾 のこり NN発」に変え、画面上端に時間の進捗バーを足した ⑤結果では海を小さくし、結果パネルを画面内へスクロールする。⑤は1200×943で下端967pxと実測してから直した
体験評価2周目(15/20・不合格)で、順位の物差しそのものが違っていたと分かった。敵船を大破させた回(ドンピシャ29)と、1発も届かなかった回(ドンピシャ0・おしい29)が、同じ「操舵手」・同じ助言で返っていた。おしいをドンピシャと同じに数えていたため。画面には「届くのはドンピシャで返した弾だけ」と書いてあるのに、順位だけがその宣言を裏切っていた。1周目に『刻みが少ない』と指摘されて段を増やしたが、それは症状で、原因は物差しだった 順位を「敵船に届いた弾の数(ドンピシャ)」だけで決めるように作り直した。おしいは順位を上げない。から振りは順位に混ぜない(から振り1回でキャプテンから落ちる段差もやめた)。評価者が挙げた実例そのものを単体テストに固定した。あわせて助言文を全段書き直し(『一発も船に当てていない』は、敵船に当てていないと読めてしまう)、連続数もドンピシャ限定にした
曲が終わってもループが回り続け、finish() が毎フレーム呼ばれていた。結果の中身を毎フレーム作り直していたうえ、結果画面から補正画面へ移ろうとしても次のフレームで結果へ引き戻され、遷移できなかった 結果と補正の状態では finish() を素通りさせた。体験評価2周目の『結果から音のズレを直す画面へ行けない』を直そうとして、ボタンを足しても動かず、そこで初めて気づいた
モバイルの余白を直したつもりが、海が伸びていなかった。枠(.stage)は694pxまで広がっていたのに、canvas は178pxのままだった。高さが flex で決まる親に対して height:100% は解決できず、canvas 自身の width/height 属性に落ちる。その属性は resize() が測った値なので、小さいまま固まって二度と戻らない canvas を絶対配置にして、親の高さの決まり方に関係なく埋めるようにした。390×844で海が224px→692px、余白は画面の61%→6%になった。あわせて『海が画面を埋めているか』『canvasが枠を埋めているか』を見るE2Eを足した
上の修正でループを止めたら、今度は結果画面のcanvasが真っ黒になった。結果に移ると海の枠が縮み、ResizeObserverがcanvasの大きさを入れ直す=中身が消える。描き直す者がいないので、50秒かけて沈めた敵船という一番の見せ場が空の箱になっていた(体験評価3周目が発見) ループは止めず、二重に結果を作らない番だけを残した。止まっている時に大きさが変わった場合に備えて、resize() 側でも描き直すようにした。E2Eで『結果画面のcanvasが単色で塗りつぶされていないこと』を固定した
CSSのセレクタを `[data-state="playing"] .page` と書いていた。data-state を持っているのは .page 自身なので、子孫セレクタでは当たらない。モバイルの余白が半分残ったまま、直ったつもりでいた `.page[data-state="playing"]` に直した。他の `[data-state] .stage` 等は .stage が子孫なので正しく効いていた。同じ書き方に見えて、当たるものと当たらないものが混ざっていた
Day 011公開中

この写真、まだ場所を覚えている

写真に残っている撮影地の座標・カメラ本体のシリアル番号・編集前の埋め込みサムネイルを危険度の高い順に見せ、JPEGの区画だけを外して画質を1バイトも変えずに保存する。写真はブラウザの中だけで処理し、外部へは通信を1本も出さない

  • exif
  • jpeg
  • privacy
制作メモ
AIに任せた
JPEGのセグメント分解・分類・除去の実装。SOS(0xFFDA)から末尾までを1バイトも変えずに写し、除去後のバイト列が元と完全一致することをユニットテストで固定した/規格を読んで分かっていた落とし穴の実装。ExifとXMPはどちらもAPP1なのでマーカー番号ではなく署名文字列で見分け、64KB超で分割された同種の区画を全部消す(最初の1つで打ち切らない)/TIFF/IFDの解析。IIとMMの両方のバイトオーダー、GPSの度分秒、IFD1のサムネイル切り出し、壊れたオフセットや異常なエントリ数で例外を投げずに失敗を返す作り/4状態(空・読込中・エラー・不正入力)+結果+対象外の切り替えと、通った順を data-state-log に残す仕組み/サンプル写真の自作(表札の住所が写った絵を描き、それを切り落とした本体に、切り落とす前の絵をIFD1のサムネイルとして差し込む)/ユニット73件・E2E42件のテスト。同一オリジン以外への通信が0本であることもE2Eで固定した/保存したバイト列をアプリ自身でもう一度読み直し、区画ごとの結果(Exifなし/ICCは残しています/画素は元のまま)を画面に出す検算。免責の文章より、その場で確かめられることのほうが効くという調査結果から足した/権利表示(Copyright / Artist)を「あなたにつながるもの」から切り離して第4のカテゴリにする組み替えと、XMPの権利表示(dc:rights・xmpRights:UsageTerms など)を文字列一致だけで拾う判定/個人情報保護法・著作権法(権利管理情報)・製造物責任法・消費者契約法・電気通信事業法の一次情報にあたる調査と、同種の日本語ツール7件の実地調査。そこから「免責文言より画面設計が効く」「権利表示は利用者の利益の向きが逆なので別カテゴリにする」「権利表示だけを消す機能は作らない」を設計へ反映した
自分でやった
実装に入る前に要件(REQUIREMENTS.md)と設計(DESIGN.md:ユーザーストーリー+UX5階層)を書き切ってから実装へ渡す、という進め方の指定/免責・注意書き・警告の文言は法務レビューの結果であとで差し替えるので、1か所(lib/copy.js)にまとめておくという指示/この日の成果物をローカルに留め、gitの操作をしないという判断/法的リスクを調べたうえで作る、という進め方の指示。調査そのものはAIが行い、その結果をどう設計へ反映するかを決めた/画面・README・meta.json に、侵害を促す表現と法的評価の断定を書かないという線引き
失敗と修正
実装に入る前に公開前チェック(npm run precheck)を走らせたら、いきなり落ちた。要件定義に書いていたサンプル写真のメーカー名に実在の名前を使っていたため。このリポジトリは公開なので、そのまま作っていたらサンプルのExifにもドキュメントにも残った メーカー名を架空のもの(SAMPLE OPTICS)に変え、要件定義側にも変更理由を書き足した。サンプルに実在の名前は使わない
meta.json を draft にすると、ビルドがそのDayを dist/ へコピーしない。E2Eは配信された dist/ を見るので、そのまま書いていたらテストが全部404で落ちるところだった spec の中に、アプリのフォルダだけを配る小さなサーバーを立てた。公開に切り替えたら通常の baseURL に寄せられる
向き1件だけのExifを組み立てる関数が、文字列の '6' も受け付けていた。中で Number() に通していたため、数でない値が黙って通っていた 型そのものを見るようにした。ユニットテストが先に捕まえた
壊れたJPEGのテストで、期待した「区切りが壊れている」ではなく「JPEGではない」が返った 実装ではなくテストの題材が誤っていた(先頭3バイトがJPEGでないバイト列を使っていたので、区切りを読む前に弾かれていた)。テスト側を直した
Exifの中身が壊れたJPEGを、まとめて「読めません」のエラー画面へ送っていた。区画を落とすこと自体はExifの解析結果に関係なくできるので、いちばん消したい写真だけが消せない状態だった。しかも文言のほうには「区画そのものは取り除けます」と書いてあり、画面が文言の約束を果たしていなかった エラーは「JPEGとして最後まで読めない」場合だけに絞り、中身が読めないだけの写真は結果の画面で扱って保存ボタンを出すようにした。見出しも「見つかりませんでした」と混ざらないよう別の文にした
著作権表示と撮影者名を「あなたにつながるもの」に入れていた。GPSやシリアル番号と同じ束に入ると、利用者には「全部消すのが正解」に見える。他人が撮った写真の権利表示を、何も考えずに消す導線になっていた 利用者にとっての利益の向きが逆(消したい情報ではなく残したい情報)なので、第4のカテゴリとして切り離した。警告は除去ボタンの真上に出したままにし、検出した値をコピーして控えられるようにした
XMPから拾った権利表示の並び順が、ファイルの中でどのセグメントが先に来るかで変わっていた。強い警告になるもの(利用条件・権利者の連絡先)が後ろに回ることがあった 見つけた名前を集めてから、こちらが決めた順(強いものが先)に並べ直すようにした。ユニットテストが先に捕まえた
画面に本体の写真を出していないのに、サムネイルの説明だけが「本体の写真と見比べてください」と言っていた。見比べる相手が画面に無いので、このアプリ最大の見せ場(切り落としたはずの表札がサムネイルにだけ残っている)が伝わらない。しかも同じ説明文が、サムネイルの行とカードの下で二重に出ていた 本体の写真と埋め込みサムネイルを並べて出し、それぞれに「いま見えている写真」「埋め込まれたサムネイル」のラベルを付けた(本体も blob URL で出すので、通信は増えていない)。説明文はカード側で1回だけにした。行側の文言を固定していたユニットテストは、『同じ説明を2か所で言わない』を見る形に書き換えた
「メタデータを消して保存」が390pxで上から3,093px=全長の68%の位置にあり、主要な行動に届くまで約4.5画面ぶんスクロールが要った 判定カードの直下へ動かした(390pxで1,125px=全長の26%)。ただし権利表示の警告は保存ボタンの直上・常時表示のまま(著作権法まわりの要件)、免責は「完全には消せない」「見つからない=無い ではない」の2点だけを上に残し、全文は保存ボタンより下へ移した。画面下に固定バーを出す案は採らなかった。保存ボタンだけを固定バーへ移すと、警告をその直上に置くという条件が満たせなくなるため
「いちばん目立たない文字でもAAを満たす」というE2Eがあったのに、共通シェアの注記が3.2:1しかない状態を素通りさせていた。テストが color の指定値だけを読んでいて、opacity で薄まった後の見た目を見ていなかった(原因は shared/share.css の .share__note { opacity: 0.7 }) 先祖にかかっている opacity を掛け合わせ、背景と混ぜた色で測るようにテストを書き直した。わざと opacity: 0.7 に戻して落ちること(3.18)を確かめてから、正本の shared/share.css で opacity をやめ、11アプリへ同期した。共通部品なので、この1か所の直しが全アプリに効く
「開く」の行を押しやすくしたことを固定するE2Eが、最初の実行で落ちた。座標で押す前に要素を画面内へスクロールしていなかったので、390pxでは対象が画面の外にあり、クリックが別の場所に当たっていた 測る前に scrollIntoViewIfNeeded し、開いたあとも位置を測り直してから押すようにした。行の高さは25px→45pxにしてある(.group__head に上下の余白が無く、押せる高さが足りていなかった)
権利表示の警告は、面積では今も保存ボタンより大きい(390pxで警告336px対ボタン54px)。採点では「警告のほうが面積で勝っていて階層が逆転している」と指摘された点 文面は法務の調査結果そのもので削れないので、余白・枠の太さ・文字の大きさを落として視覚的に静かにし、常時表示は保ったまま、保存ボタンの側を大きく(横いっぱい・高さ54px・濃い青)した。面積の逆転そのものは解消できていない
共通シェアのボタンの枠が地色に対して1.7:1しかなく、面も1.1:1で「押せるもの」に見えていなかった。前回opacityの穴を塞いだコントラストのE2Eは、文字の色しか見ておらず枠の色を一度も測っていなかった 枠を currentColor の32%から90%へ上げ、11アプリぶんの枠と地色の比を測るE2Eを足した。濃さは勘ではなく実測で決めた:いちばん条件が厳しいDay007はシェア欄の文字色そのものが地に対して3.9〜5.1:1しかなく、75%では2.6:1で3:1に届かなかった
その新しいE2Eが、最初は嘘の合格を出していた。color-mix() の計算結果を Chromium は color(srgb 0.93 0.94 0.96 / 0.75) の形で返す(成分が0〜1)のに、rgb() と同じ0〜255として読んでいたため、明るい枠を真っ黒として測って19:1と報告していた color( で始まる値は255倍してから測るようにした。文字側の古いコントラストのE2Eにも同じ読み方が残っていたので、そちらも直した。テスト自身が壊れていると、直したつもりで何も守れていない状態になる
同じE2Eが、Day009だけ1.12:1という桁違いの低さを出した。背景をグラデーションで塗っているアプリでは backgroundColor がどの先祖でも透明のままで、白い紙の上として測っていた(実際は暗い画面) 先祖を遡るときに background-image のグラデーションも見て、色の停止点を全部拾い、いちばん条件の悪い停止点で判定するようにした。あわせて「枠と面の比」の判定は外した。塗りのあるボタン(--primary)は面そのものが地との境目になるので、そこまで求めると塗りを濃くできなくなる
本体写真と埋め込みサムネイルを狭い画面でも横に並べたら、ラベルの長さが左右で違うせいで(「埋め込まれたサムネイル」だけが2行)、2枚の画像の上端が55pxずれた。このカードは見比べるためにあるのに、見比べにくいまま並べ替えただけになっていた ラベルの行と画像の行を2枚で共有する行(subgrid)にそろえ、画像の上端を一致させた。subgridが無い環境では宣言が無効になり、以前と同じ挙動に戻るだけで壊れない。E2Eも「2枚が横に並んでいること」だけでなく「画像の上端がそろっていること」を見るようにした
カードの幅を本文の折り返し幅に合わせて詰めたら、今度は「どこにも送られていません。」の最後の1文字だけが2行目に落ちた。1つ直すと別の場所に出るたぐいの崩れ 行長を32rem→33remに広げ、この行の文字を0.92rem→0.9rem(カード内の本文と同じ大きさ)にそろえた。そのうえで text-wrap: pretty を本文に掛け、最終行に1〜2文字だけ残る折り返しを全体で減らした(残っているのは3文字以上のものだけ)
権利表示の警告を保存ボタンと同じカードに入れていたので、「メタデータを消して保存」が琥珀色の箱の内側にあるように見え、警告に対するボタンだと読めていた 警告を独立したカードにして、保存ボタンのカードと分けた(間は30px)。警告を除去ボタンの直上に常時出すという要件は、隣り合うカードとして満たしている。ただし面積の逆転(390pxで警告359px対ボタン54px)は、文面が法務の調査結果そのもので削れないため今回も解消していない
保存ボタンを上げすぎて、検出内容を見ないまま保存できる順序になっていた。採点の「CTAが68%地点」を額面どおり受け取り、判定カードの直下へ動かした結果、検出内容(分類カード)と本体写真・サムネイルの見比べが保存ボタンより下に落ちた。このアプリは「何が入っていたかを確かめてから消す」ためのものなので、いちばん見せたい中身を飛ばして主要な行動に到達できる並びは、それ自体が不具合だった。深い位置にあったのが問題だったのではなく、上にあったのが説明文ばかりで中身が無かったのが問題だったと読み違えていた 並びを「判定 → 検出内容 → 写真の見比べ → 付随情報 → 限界の要点 → 権利表示の警告 → 保存 → 詳細」に直した。保存ボタンは390pxで判定から2,527px下・ページ全長の68%地点に戻ったが、上にあるのは利用者が見たい中身そのものなので、この深さは意図どおり。法務由来の2条件(追記2 G-2 の限界の要点/追記3 K-3 の警告の直上表示)はどちらも保存ボタンより上のまま維持している。結果カードの直下の子要素の並びをそのままE2Eで固定し、説明の壁を保存ボタンの前に積み直せないようにした
本体写真とサムネイルの見比べで、画像の上端だけをそろえて済ませていた。実際には表札(ラベル)の行数が左右で違うせいで「8.4KB」が約25px下にずれ、2枚の画像の下端も約29pxずれていた。見比べるための機能なのに、上端以外に共通の基準線が無かった 表札・大きさ・画像の3行を2枚で共有する行にそろえ、表札には2行ぶんの高さを最初から与えて行数を固定した(subgridが無い環境でもずれない)。画像は共通の 4:3 の枠に object-fit: contain で収め、上端と下端の両方を一致させた。E2Eは上端だけでなく、下端・表札の高さ・大きさの表示の位置も測るようにした(いずれも実測0px差)
Day 010公開中

いま、誰かが書き直している

世界中のウィキペディアで、いまこの瞬間に書き換えられている記事の「場所」が世界地図に立つ。いちばん書き換わっている国を名指しし、日本語版の編集は記事名まで読める

  • server-sent-events
  • wikipedia
  • world-map
制作メモ
AIに任せた
実装前にユーザーストーリーとUX5階層を書き、そこから設計を決めた(本人指示でこの工程をハーネスに恒久追加)/候補3案のデータ源が本当に生きているか(認証不要・CORS許可・応答200)を提案前に実測/「編集者の場所は原理的に出せない(IPが要る=方針違反)」ことと「記事そのものの座標なら出せる」ことを実測で切り分け/編集の18%しか座標を持たないことを実測し、地図が生きる頻度(毎秒約1本)を見積もった/Natural Earthの国境を間引いて63KBに収める道具、正距円筒の投影、点と国の判定、問い合わせを絞る仕組み、ユニット49項目とE2E18項目の実装
自分でやった
3候補から作るものを選ぶ判断/水面版を世界地図版へ作り直す発案と、その日のうちに公開まで進める判断/ウィキペディアのAPIへ継続的に問い合わせることを、範囲を絞ったうえで許容する判断/UI・UXを作るときは毎回ユーザーストーリーとUX5階層を先に書く、というルールの制定
失敗と修正
「世界のウィキペディアは毎秒30件書き換わっている」と説明にも投稿文にも書いていた。実測30秒間の916件をそのまま割った数字だったが、中身はカテゴリ操作287件とログ227件を含んでいて、このアプリが「編集」として数えているものではなかった 内訳を数え直した。編集と新規作成だけなら毎秒13.4件で、別の時間帯の再実測でも13.6〜14.1件で一致した
ピンの位置を画面の座標で持っていたので、ウィンドウの幅を変えると既に立っているピンが全部ずれた場所に残った。スマホ幅のスクリーンショットで、アフリカ沖の海の上にピンが並んで初めて気づいた ピンは緯度経度だけを持ち、画面の座標は描くたびに計算するようにした。4都市に立てて3つの画面幅で位置が変わらないことをE2Eで固定した
経度360度・緯度142度を枠いっぱいに引き伸ばして描いていたので、アフリカと南米が縦長に潰れていた 縦横の比を保った矩形を枠の中に置き、余りを余白にした。枠からはみ出す南極も切り取るようにした
地図に立つピンと「世界の編集数」が一桁違って見えた。座標を持つ記事は全体の2割弱しかないのに、その説明がどこにも無かった 「場所を持たない記事の編集◯件は地図に出せません」を常に出すようにした。UX5階層の構造層を書いている段階で気づいた
点の薄れ方を (1-t)^2 で書いていたので、立った直後がいちばん速く薄れて、直近のピンほど目立たなくなっていた 1-t^2 に変えて、最初はほとんど薄れず終わりぎわで消えるようにした。ユニットテストが先に捕まえた
Day 009公開中

秋田バスライブ

いま秋田県を走っているバスの実測GPS位置を、自前の3Dマップの上で走らせる。路線網は県内17事業者のGTFS-JPから作り、実測位置はGTFS-RTを20秒ごとに中継する

  • gtfs-realtime
  • canvas
  • 3d
制作メモ
AIに任せた
GTFS-RTのProtocol Buffersを外部ライブラリなしで読む中継API(Cloudflare Pages Functions)の実装/緯度経度→平面→カメラ回転→透視投影の自前3Dパイプラインと、20秒ごとの位置を補間して走らせる処理/17事業者のGTFS-JPから路線網を抽出するツールと、ユニットテスト・E2E・スクリーンショット撮影の整備
自分でやった
外部APIとサーバー処理を解禁する判断と、実測位置が2事業者しかない現実を隠さずに見せる方針の決定
失敗と修正
秋田市のフィードは数十秒ごとに作り直されているのに、中の車両の位置送信時刻が9時間41分前で止まったままだった。そのまま描くと動かないバスが夜通し『走行中』に見える 中継APIで10分以上古い位置を除外し、除外した台数を画面に出すようにした
上の10分ルールを入れた数時間後、同じ秋田市のフィードが『座標はビット単位で12時間以上まったく同じまま、送信時刻だけを現在時刻に更新する』挙動に変わり、除外をすり抜けた。公開直後の本番で深夜に『1台が走行中です』と表示していた 時刻の新しさだけを信用するのをやめ、同梱した時刻表の運行時間帯と突き合わせて、その事業者の運行時間外に届く車両は走行中に数えないようにした。除外の理由を2種類に分けて画面に出している
17事業者のうち12事業者は道なりの路線形状を公開しておらず、停留所を直線で結んだ角ばった線にしかならない 実形状と近似を線の太さと破線で描き分け、17事業者中5事業者しか形状を公開していないことを画面に明記した
Day 008公開中

じはんきレーダー

近くの自動販売機を方角と距離のレーダーで示す。OpenStreetMap収録の全国データを同梱し、位置情報は端末の中だけで処理する

  • geolocation
  • canvas
  • web-audio
制作メモ
AIに任せた
OpenStreetMapの自販機データ取得・日本クリップ・軽量化パイプラインの実装/レーダー描画・8方位と距離の計算・ソナー音合成・テストの実装
自分でやった
近くの自販機を探すアプリという題材の発案と、収録データが実際の約1.3%しかない前提で進める判断
失敗と修正
Overpass APIが混雑していて、全国データの取得が最初は何度もタイムアウトした メインとミラーを順に切り替えて再試行する取得スクリプトにして、日本全域の絞り込みクエリで取り切った
デモ動画の収録ツールがページをfile://で開くため、ESモジュールとfetchを使うこのアプリは録画中に動かなかった 振り付けスクリプト内で小さなローカルHTTPサーバーを立て、そこから開き直して収録した
Day 007公開中

ひといきピアノ

クリック、タッチ、PCキーで演奏できる25鍵のWebピアノ。和音、サステイン、音量変更に対応し、音はWeb Audio APIで端末内合成する

  • music
  • web-audio
  • vanilla-js
制作メモ
AIに任せた
Web Audio APIによる音声合成、25鍵のUI、和音・サステイン・音量・全停止の実装/PC・スマートフォン相当の画面で操作確認し、X投稿用のデモ動画と投稿文を作成
自分でやった
Webで弾けるピアノを今日のアプリに選び、動画とX投稿素材も作る判断
失敗と修正
100日チャレンジ本体ではなく、別アカウントの単独リポジトリにPRを作ったため一覧へ反映されなかった 本人の指摘を受けて正しい個人用リポジトリを確認し、Day 007として移植した
Day 006公開中

構造化データ、いま出るやつだけ

ページの種類と情報を入れると、貼れる形のJSON-LDが出る。ただし2026年8月時点でGoogleがサポートしているタイプだけ。廃止済みのFAQとHowToは生成せず、廃止された日と理由を出す

  • seo
  • json-ld
  • vanilla-js

所要時間1時間10分

制作メモ
AIに任せた
Google検索セントラルで現行の対応タイプ一覧と、LocalBusiness・Article・BreadcrumbListの必須/推奨プロパティを確認/FAQとHowToのリッチリザルトが廃止された日付を一次情報で特定(FAQ=2026-05-07、HowTo=2023-09-13)/index.html・style.css・script.jsの実装(Codexサブエージェントに委任)/Playwrightで47項目の出力検証スクリプトを書いて実行/公開前にUXを実測(コントラスト比・タップ領域・支援技術への伝達)し、見つかった課題4件を修正してアクセシビリティ23項目で再検証
自分でやった
4候補(帰省スマホ設定チェックリスト/定例会議の年間コスト計算機/睡眠負債の可視化/構造化データ生成)から作るものを選ぶ判断/公開まで進める判断。コミット・PR作成・デプロイはClaudeに任せ、mainへのマージだけは自分の手で行う運用を維持した
失敗と修正
HowToの廃止時期を「2023年8月の変更で縮小され、その後サポート対象から外れた」と曖昧なまま実装に渡してしまった。廃止日を主題にするアプリなのに、肝心の日付が無かった 一次情報を当たり直し、2023年8月にモバイルでの表示が終わってデスクトップ限定になり、その1か月後の2023年9月13日にデスクトップからも消えた、と確定させた。ただし実装を委任した後で要件を直したため、返ってきたコードは古い記述のままだった(実装側は「要件から外した点はない」と報告してきたが、見ていた要件そのものが古い)。委任中に要件を書き換えたのが原因で、渡す前に凍結するべきだった
廃止を告げる見出しで「これは2026年5月7」「日に廃止されました」と日付の途中で改行し、このアプリの主題である日付が読めなくなっていた。デモ動画を撮って初めて気づいた 見出しを1つの文字列で持つのをやめ、日付だけを nowrap のspanで包む形に変えた。文字サイズや画面幅が変わっても日付が割れない
公開前にUXを実測したら、このアプリの主張である「廃止」が打ち消し線と灰色だけで表現されていた。読み上げでは現行のタイプと区別がつかず、色覚特性のある人にも弱い。あわせてライトモードの小さい文字が4箇所でコントラスト比3.36〜3.68:1しかなく、WCAG AAの4.5:1に届いていなかった(ダークモードは全部合格していた) 廃止の2ボタンに「廃止」の文字バッジを足して、線と色だけに頼るのをやめた。灰色は #788983 から #5F726C に濃くして、紙の上で4.66:1・カードの上で5.11:1にした。ついでにコピーボタンの高さを36pxから44pxにし、種類の5択を role=radiogroup にして矢印キーだけで選べるようにした。営業時間の平日一括入力は機能追加なので今回は入れていない
Day 005公開中

その写真、何色でできてる?

画像をドロップすると主要な6色を取り出して、カラーコードをタップでコピーできる。処理はすべてブラウザの中だけで、画像はどこにも送信されない

  • canvas
  • design-tool
  • vanilla-js

所要時間40分

制作メモ
AIに任せた
メディアンカット法の選定と実装/サンプル画像3種を実行時にcanvasへ手続き描画するコード(画像ファイルを同梱しないための設計)/要件定義の下書きと実装
自分でやった
4候補(帰省スマホ設定/会議コスト計算機/カラーパレット抽出/構造化データ生成)から作るものを選ぶ判断
失敗と修正
サンプル「夜の街」の初版は、細い窓のネオンが120pxへの縮小で壁の色と混ざり、パレットが紺系ばかりになった。ピンクもシアンも1色も出ない ビル1棟の窓を同系色に寄せ、壁いっぱいの大型ビジョンと路面の反射を面で入れて、縮小後もピンク・シアン・アンバーが残る構図に描き直した
べた塗り3色の画像で、同じHEXのスウォッチが2枚並んだ。メディアンカットは画素数で箱を割るので、純色だけの箱と純色+境界画素の箱が同じ平均色になることがある 平均色が同一HEXになった箱は1つにまとめて画素数を合算した。まとめた分だけ色数は6より減るので、コピーのボタン表示も実際の色数に追従させた
Day 004公開中

奨学金、何歳まで返す?

奨学金の返還年数は自分で選べず、借りた総額で自動的に決まる。JASSOの算出表どおりに計算して、返し終わる年齢と毎月の返還額を出す

  • calculator
  • vanilla-js
  • no-dependencies

所要時間30分

制作メモ
AIに任せた
JASSOの返還年数算出表と返還例の調査/第二種の計算式を公式返還例60件から逆算し、6モデルを比較して決定/要件定義と実装/公式返還例70件との突き合わせ検証
自分でやった
3案(会議コスト計算機/奨学金返還シミュレータ/構造化データ生成)から作るものを選ぶ判断
失敗と修正
最初の提案時、返還年数をユーザーが自由に選べる前提で画面を描き、利率も0.9%という古い値を使っていた。実際のJASSOは返還年数を貸与総額から自動決定し、利率も大幅に上がっていた 公式の算出表を取得して実装し直した。第一種は公式返還例10件と完全一致することを確認。利率は変動するため断定表示をやめ、公式返還例と同じ0.5/1/2/3%を選択肢にした
第二種を単純な元利均等で計算すると、公式の返還例より最大1.2%低く出た 据置期間の扱いが違う6つのモデルを公式60件に当てて比較し、誤差が最小(総額+0.03%以内)の「据置6か月分の利息を別途上乗せ」を採用した
ブラウザで確認したら、第一種を選んでも利率欄が消えなかった。DOMではhidden=trueなのに表示されていた 自作の .field { display:flex } が、ブラウザ標準の [hidden]{display:none} に勝っていた。[hidden]{display:none !important} で打ち消した
公開前のファクトチェックで、第一種の貸与月額から50,000円(私立・自宅外)が抜けていることが分かった。私立大学で自宅外通学の学生が自分の額を選べない状態だった JASSOの月額表を4区分すべて取得し直し、和集合の8段に修正。実際に選べない額が混じることはREADMEと画面に明記した
リーガル確認で、JASSO公式ツールとの誤認を防ぐ表示と、損害についての免責が無いことが分かった ヘッダーに非公式である旨を常時表示し、末尾に免責を追加。JASSOのサイトポリシーでリンクは自由と明記されていることも確認した
JASSOのサイトポリシーに「許諾を受けていない無断転用は禁止」とあり、算出表16行を画面に再現している点が先方の掲示に反する可能性があった(著作権法上のリスクは低いと判断したものの) 本人判断で表の全体再現をやめ、該当する1区分と実際の割り算だけを見せる形に変更。数字の羅列ではなく年数が決まる手順そのものが読めるようになり、結果的に主題は強まった
デモ動画を撮って気づいた。第一種から第二種へ切り替えると、月額が64,000円から90,000円へ勝手に飛んでいた。内部でスライダーのインデックスだけを共有していたのが原因 状態をインデックスではなく金額そのもので持ち、切り替え先に同じ額が無ければ最も近い額に寄せるようにした(64,000→60,000)
Day 003公開中

タップ花火

夜空をタップした場所に花火が打ち上がるおもちゃ。合成音つき・長押しで連発・放置でも自動で上がる

  • canvas
  • animation
  • vanilla-js

所要時間30分

制作メモ
AIに任せた
要件定義と実装
自分でやった
作るもの・機能範囲の判断/1作目を不採用にする判断
失敗と修正
1作目のスクショ整形が微妙 夜に花火へ作り直し
Day 002公開中

ドット絵メーカー

画像をドロップするとドット絵に変換してPNG保存できるツール

  • image
  • canvas
  • vanilla-js

所要時間15分

制作メモ
AIに任せた
要件定義と実装
自分でやった
作るもの・機能範囲の判断
Day 001公開中

集中タイマー

時間をセットして開始・一時停止・リセットできる集中用カウントダウンタイマー

  • timer
  • vanilla-js
  • no-dependencies

所要時間10分

制作メモ
AIに任せた
要件整理と実装
自分でやった
作るものと機能範囲の判断
失敗と修正
Cloudflareキャッシュ混入 公開前に除外

ABOUT

大学生。毎日1個Webアプリを作って公開しています。
アカウント登録なし・課金なし・個人を特定できるデータは扱わない。作れるものの幅はDayが進むほど解禁していきます。

実際に操作しているところを録画したデモです