こんにちは、とまだです。
クソアプリ Advent Calendar 2024、22 日目の記事をお届けします!
突然ですが、みなさんこんな経験ありませんか?
- 「この文章イマイチだな...」と思いながら書いている
- 「どうせボツになるし...」と諦めながら入力している
- 「もう全部消しちゃおうかな...」と考えながらタイピングしている
そう、文字を入力することへの後悔です。
そこで考えました。
「最初から文字が散らばるエディターを作れば、後悔する必要がないのでは?」
むしろ、文字が跳ね回って読めなくなることを仕様にしてしまえば、内容を気にする必要すらない!
そんなクソみたいな発想から、このエディターは生まれました。
💡 できたもの
上の入力欄に文字を入力すると、文字が物理演算で跳ね回ります。
日本語入力にも対応しており、Enter を押すと文字が飛び散ります。
↓ デモ
🔧 実装のポイント
せっかくなので、実装の中で特に注意した点を共有します。
1. IME 制御の罠との戦い
日本語入力周りは、想像以上に泥沼でした。
半角入力に対応するだけならシンプルなのですが、日本語入力は「変換中」「確定」「未確定」などの状態があり、それぞれの状態で文字列が異なります。
const handleKeyDown = (e: React.KeyboardEvent) => {
// IMEの入力中は処理しない
if (composing) return
// Enter キーの処理
if (e.key === 'Enter') {
e.preventDefault()
if (inputValue && !composing) {
addChars(inputValue)
setInputValue('')
}
return
}
// ...
それぞれを考慮した上で、入力された文字列を適切に処理する必要があります。
実務でもハマったことがあるポイントではありますが、今回は特に難しかったです。
2. 物理演算の実装
物理演算は、以下のような要素を実装しています。
- 重力加速度
- 空気抵抗
- 床との衝突判定
- 速度の減衰
流石に不慣れだったので、今回は ChatGPT 先生に大いに助けられました。
最終的に、物理演算の核となる部分は以下のようになっています。
こういうときにも useEffect
が使えると知れたのは良い学びです。
// 物理演算の核となる部分
useEffect(() => {
// 静止状態の場合は計算をスキップ
if (isStatic) return;
// 物理パラメータの設定
const gravity = params.gravity; // 重力加速度
const bounce = -0.7; // 反発係数(衝突時のエネルギー損失)
const friction = 0.99; // 摩擦係数(地面との接触時の減速)
// 約60FPS (1000ms/60 ≈ 16ms) でフレーム更新
const interval = setInterval(() => {
setPosition((pos) => {
// 速度に基づいて新しい位置を計算
const newX = pos.x + velocity.x;
const newY = pos.y + velocity.y;
// 左右の壁との衝突判定(x座標が±100を超えた場合)
if (newX < -100 || newX > 100) {
setVelocity((v) => ({ ...v, x: v.x * bounce }));
}
// 地面との衝突判定(y座標が20を超えた場合)
if (newY > 20) {
// 地面との衝突時の処理
setVelocity((v) => ({
x: v.x * friction, // 水平方向に摩擦を適用
y: v.y * bounce, // 垂直方向に反発を適用
}));
// 垂直方向の速度が十分小さくなった場合、静止状態とみなす
if (Math.abs(velocity.y) < 0.5) {
setIsStatic(true);
}
// 地面の位置(y=20)で固定
return { x: newX, y: 20 };
}
// 通常時は重力を適用
setVelocity((v) => ({ x: v.x, y: v.y + gravity }));
// 新しい位置を返す
return { x: newX, y: newY };
});
}, 16);
// コンポーネントのクリーンアップ時にインターバルを解除
return () => clearInterval(interval);
}, [velocity, isStatic, params.gravity]);
3. パフォーマンスとの戦い
当初はすべての文字に対して物理演算を行っていましたが、文字数が増えるとブラウザが悲鳴を上げ始めました。
そこで以下のような対策を行いました。
- 文字の数に制限を設ける
- 速度が小さくなった文字は静止状態にする
- requestAnimationFrame の使用
- useCallback と useMemo の活用
// パフォーマンス改善前
const animate = () => {
chars.forEach((char) => {
updatePosition(char.position, char.velocity);
});
};
// パフォーマンス改善後
const animate = () => {
chars.forEach((char) => {
if (!char.isStatic) {
// 動いている文字だけ更新
updatePosition(char.position, char.velocity);
// 速度が小さければ静止状態に
if (isAlmostStopped(char.velocity)) {
char.isStatic = true;
}
}
});
};
🎮 ソースコード
実際のコードは GitHub で公開しています。
スターを付けていただいても、文字は跳ね返ります。
[リポジトリへのリンク]
🐛 既知の問題
- 大量の文字を入力するとブラウザが固まる
- たまに文字が宇宙まで飛んでいく(仕様です)
- コピー&ペーストすると死ぬ
- 文字が画面外に消えることがある(探さないでください)
📝 まとめ
クソアプリを作って気づいたこと。
- クソアプリこそ真面目に作るべき
- IME 周りは想像以上に複雑
- 物理演算は楽しい
- パフォーマンス改善は大事
- 無駄な機能を真面目に作ると学びが多い
最後に、ここまで読んでいただいた方、ありがとうございました!
この記事を読んで、「自分も何か作ってみよう」と思っていただけたら嬉しいです。
他にもアドベントカレンダー記事を書いています!
他にも、2024 年のアドベントカレンダーに参加しています。
以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!