テキスト解読(スクランブル)

ランダムな記号から正しい文字へ左から順に解読されていく演出をJSで実装。ハッカー風・読み込み完了の表現に向いています。

#js#css#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="page">
  <header class="nav">
    <div class="brand"><span class="logo"></span>FlowDesk</div>
    <span class="status"><i></i>すべてのシステムが正常</span>
  </header>

  <section class="panel">
    <p class="eyebrow">DEPLOYING WORKSPACE</p>
    <h1 class="scramble" data-final="FlowDesk Ready."></h1>
    <div class="bars">
      <div class="row"><span>ワークスペース初期化</span><i class="ok">完了</i></div>
      <div class="row"><span>データ同期</span><i class="ok">完了</i></div>
      <div class="row"><span>公開URLの発行</span><i class="run">処理中</i></div>
    </div>
    <button class="redo">再デプロイ</button>
  </section>
</div>
CSS
/* FlowDesk:デプロイ完了のスクランブル解読が主役 */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(700px 360px at 50% -10%, #16284d 0%, transparent 60%),
    linear-gradient(160deg, #0f1b34 0%, #0b1326 100%);
  color: #e7ecf7;
  min-height: 400px;
  overflow: hidden;
}
.page { padding: 16px 26px; }

.nav { display: flex; align-items: center; justify-content: space-between; }
.brand { display: flex; align-items: center; gap: 9px; font-weight: 700; font-size: 16px; }
.logo {
  width: 17px; height: 17px; border-radius: 6px;
  background: linear-gradient(135deg, #4f7cff, #8ab4ff);
  box-shadow: 0 0 10px rgba(79,124,255,0.6);
}
.status { display: flex; align-items: center; gap: 7px; font-size: 12px; color: #8593b5; }
.status i { width: 8px; height: 8px; border-radius: 50%; background: #28c840; box-shadow: 0 0 8px #28c840; }

.panel { text-align: center; padding: 28px 6px 0; }
.eyebrow { font-size: 11px; letter-spacing: 0.34em; color: #6f86c2; font-weight: 700; }

/* スクランブル本体(等幅で揺れを抑える) */
.scramble {
  margin-top: 14px;
  font-family: "Consolas", "SFMono-Regular", monospace;
  font-size: clamp(30px, 6.5vw, 52px);
  font-weight: 800; letter-spacing: 0.01em;
  color: #8ab4ff;
  min-height: 1.2em;
}
.scramble .roll { color: #4f7cff; opacity: 0.75; }
.scramble .lock { color: #eaf1ff; }

.bars {
  margin: 22px auto 0; max-width: 320px;
  text-align: left;
}
.row {
  display: flex; justify-content: space-between; align-items: center;
  font-size: 13px; color: #c3cee6;
  padding: 9px 0; border-bottom: 1px solid #1d2c4f;
}
.row i { font-size: 11px; padding: 3px 9px; border-radius: 14px; font-style: normal; }
.ok { color: #28c840; background: rgba(40,200,64,0.12); }
.run { color: #febc2e; background: rgba(254,188,46,0.12); }

.redo {
  margin-top: 20px;
  background: #4f7cff; color: #fff; border: none;
  padding: 9px 20px; border-radius: 9px; font-size: 13px; font-weight: 600;
  cursor: pointer;
}
.redo:hover { background: #6a90ff; }
JavaScript
// デプロイ完了テキストをスクランブル解読する
(function () {
  const el = document.querySelector('.scramble');
  const btn = document.querySelector('.redo');
  if (!el) return; // null安全

  const finalText = el.dataset.final || '';
  const CHARS = '!<>-_\\/[]{}=+*?#01ABCXYZ';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let raf = null;

  // ランダムな1文字
  const rand = () => CHARS[Math.floor(Math.random() * CHARS.length)];
  // HTML特殊文字を無害化
  const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

  function run() {
    if (raf) cancelAnimationFrame(raf);
    if (reduce) { el.textContent = finalText; return; } // 控えめ設定は即確定

    // 文字ごとに確定フレームをずらし、左から解読
    const chars = [...finalText].map((c, i) => ({
      to: c, revealAt: i * 3 + Math.floor(Math.random() * 8) + 5
    }));
    let frame = 0;

    function tick() {
      let html = '', done = 0;
      for (const ch of chars) {
        if (frame >= ch.revealAt) {
          html += '<span class="lock">' + esc(ch.to) + '</span>';
          done++;
        } else {
          html += '<span class="roll">' + esc(rand()) + '</span>';
        }
      }
      el.innerHTML = html;
      frame++;
      if (done < chars.length) raf = requestAnimationFrame(tick);
    }
    tick();
  }

  run();
  if (btn) btn.addEventListener('click', run);
})();

コード

HTML
<main class="stage">
  <p class="pre">DECODING...</p>
  <!-- data-final が最終的に解読される文字列 -->
  <h1 class="scramble" data-final="UNLOCKED"></h1>
  <button class="redo" type="button">再解読</button>
</main>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  background:
    radial-gradient(700px 400px at 50% 50%, #05231b 0%, #04130f 70%, #020a08 100%);
  font-family: "Consolas", "Courier New", monospace;
  color: #29ffb0;
  overflow: hidden;
  padding: 24px;
}

.stage { text-align: center; }

.pre {
  font-size: 12px;
  letter-spacing: 0.45em;
  color: #0f8a63;
  margin-bottom: 16px;
  padding-left: 0.45em;
}

.scramble {
  font-size: clamp(40px, 11vw, 90px);
  font-weight: 700;
  letter-spacing: 0.14em;
  /* 等幅でガタつかないように最小幅を確保 */
  min-height: 1.2em;
  text-shadow: 0 0 12px rgba(41, 255, 176, 0.55);
}

/* まだ確定していない文字(スクランブル中)は淡く */
.scramble .lock { color: #29ffb0; }
.scramble .roll { color: #1f9c70; opacity: 0.8; }

.redo {
  margin-top: 30px;
  font-family: "Consolas", monospace;
  font-size: 13px;
  letter-spacing: 0.1em;
  color: #29ffb0;
  background: transparent;
  border: 1px solid rgba(41, 255, 176, 0.4);
  padding: 9px 22px;
  border-radius: 4px;
  cursor: pointer;
  transition: background .2s, box-shadow .2s;
}
.redo:hover {
  background: rgba(41, 255, 176, 0.1);
  box-shadow: 0 0 16px rgba(41, 255, 176, 0.25);
}
JavaScript
// テキストスクランブル(解読)演出
(function () {
  const el = document.querySelector('.scramble');
  const btn = document.querySelector('.redo');
  if (!el) return; // null安全

  const finalText = el.dataset.final || '';
  const CHARS = '!<>-_\\/[]{}—=+*^?#01ABCXYZ';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let raf = null;

  // ランダムな記号を1つ返す
  const rand = () => CHARS[Math.floor(Math.random() * CHARS.length)];

  function run() {
    if (raf) cancelAnimationFrame(raf);

    if (reduce) {
      // モーション控えめ: 即確定
      el.textContent = finalText;
      return;
    }

    // 文字ごとに「確定するフレーム」をずらして左から解読されるように
    const chars = [...finalText].map((c, i) => ({
      to: c,
      revealAt: i * 4 + Math.floor(Math.random() * 8) + 6
    }));

    let frame = 0;

    function tick() {
      let html = '';
      let done = 0;
      for (const ch of chars) {
        if (frame >= ch.revealAt) {
          html += '<span class="lock">' + escape(ch.to) + '</span>';
          done++;
        } else {
          html += '<span class="roll">' + escape(rand()) + '</span>';
        }
      }
      el.innerHTML = html;
      frame++;
      if (done < chars.length) {
        raf = requestAnimationFrame(tick);
      }
    }
    tick();
  }

  // HTML特殊文字を無害化
  function escape(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  run();
  if (btn) btn.addEventListener('click', run);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「テキスト解読(スクランブル)」の効果を追加してください。

# 追加してほしい効果
テキスト解読(スクランブル)(タイポグラフィ)
ランダムな記号から正しい文字へ左から順に解読されていく演出をJSで実装。ハッカー風・読み込み完了の表現に向いています。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<main class="stage">
  <p class="pre">DECODING...</p>
  <!-- data-final が最終的に解読される文字列 -->
  <h1 class="scramble" data-final="UNLOCKED"></h1>
  <button class="redo" type="button">再解読</button>
</main>

【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  background:
    radial-gradient(700px 400px at 50% 50%, #05231b 0%, #04130f 70%, #020a08 100%);
  font-family: "Consolas", "Courier New", monospace;
  color: #29ffb0;
  overflow: hidden;
  padding: 24px;
}

.stage { text-align: center; }

.pre {
  font-size: 12px;
  letter-spacing: 0.45em;
  color: #0f8a63;
  margin-bottom: 16px;
  padding-left: 0.45em;
}

.scramble {
  font-size: clamp(40px, 11vw, 90px);
  font-weight: 700;
  letter-spacing: 0.14em;
  /* 等幅でガタつかないように最小幅を確保 */
  min-height: 1.2em;
  text-shadow: 0 0 12px rgba(41, 255, 176, 0.55);
}

/* まだ確定していない文字(スクランブル中)は淡く */
.scramble .lock { color: #29ffb0; }
.scramble .roll { color: #1f9c70; opacity: 0.8; }

.redo {
  margin-top: 30px;
  font-family: "Consolas", monospace;
  font-size: 13px;
  letter-spacing: 0.1em;
  color: #29ffb0;
  background: transparent;
  border: 1px solid rgba(41, 255, 176, 0.4);
  padding: 9px 22px;
  border-radius: 4px;
  cursor: pointer;
  transition: background .2s, box-shadow .2s;
}
.redo:hover {
  background: rgba(41, 255, 176, 0.1);
  box-shadow: 0 0 16px rgba(41, 255, 176, 0.25);
}

【JavaScript】
// テキストスクランブル(解読)演出
(function () {
  const el = document.querySelector('.scramble');
  const btn = document.querySelector('.redo');
  if (!el) return; // null安全

  const finalText = el.dataset.final || '';
  const CHARS = '!<>-_\\/[]{}—=+*^?#01ABCXYZ';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let raf = null;

  // ランダムな記号を1つ返す
  const rand = () => CHARS[Math.floor(Math.random() * CHARS.length)];

  function run() {
    if (raf) cancelAnimationFrame(raf);

    if (reduce) {
      // モーション控えめ: 即確定
      el.textContent = finalText;
      return;
    }

    // 文字ごとに「確定するフレーム」をずらして左から解読されるように
    const chars = [...finalText].map((c, i) => ({
      to: c,
      revealAt: i * 4 + Math.floor(Math.random() * 8) + 6
    }));

    let frame = 0;

    function tick() {
      let html = '';
      let done = 0;
      for (const ch of chars) {
        if (frame >= ch.revealAt) {
          html += '<span class="lock">' + escape(ch.to) + '</span>';
          done++;
        } else {
          html += '<span class="roll">' + escape(rand()) + '</span>';
        }
      }
      el.innerHTML = html;
      frame++;
      if (done < chars.length) {
        raf = requestAnimationFrame(tick);
      }
    }
    tick();
  }

  // HTML特殊文字を無害化
  function escape(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  run();
  if (btn) btn.addEventListener('click', run);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。