5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【クソアプリ】文字が跳ねまくって収集がつかないエディタを作ってみた 🦘

Last updated at Posted at 2024-11-09

こんにちは、とまだです。

クソアプリ Advent Calendar 2024、22 日目の記事をお届けします!

突然ですが、みなさんこんな経験ありませんか?

  • 「この文章イマイチだな...」と思いながら書いている
  • 「どうせボツになるし...」と諦めながら入力している
  • 「もう全部消しちゃおうかな...」と考えながらタイピングしている

そう、文字を入力することへの後悔です。

そこで考えました。

「最初から文字が散らばるエディターを作れば、後悔する必要がないのでは?」

むしろ、文字が跳ね回って読めなくなることを仕様にしてしまえば、内容を気にする必要すらない!

そんなクソみたいな発想から、このエディターは生まれました。

💡 できたもの

上の入力欄に文字を入力すると、文字が物理演算で跳ね回ります。

日本語入力にも対応しており、Enter を押すと文字が飛び散ります。

↓ デモ

画面収録-2024-11-06-20.17.51.gif

🔧 実装のポイント

せっかくなので、実装の中で特に注意した点を共有します。

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 年のアドベントカレンダーに参加しています。

以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?