1
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?

乃木坂の神曲をWordcloud風にアニメーション化してみた

Last updated at Posted at 2025-05-31

元ネタはこちら↓

なぜ行おうとしたか

元ネタでは、単なる静的なWordCloudでは、見た目にインパクトが少ないし、読者の目もすぐに離れてしまいがちです(viewsの数が少なかった😂)
動きがあれば注目を集めやすく、より楽しんでもらえるのではないかと思いました(安易ですが)

結果

これ_compressed (1).gif

YouTube short こちらの方が見やすいです↓
YouTube動画

……でも、むずかしかったー!
いやほんと、簡単に動かせると思ってたんですが、甘かったです

• WordCloud自体にアニメーション機能がない
• Python以外でやるなら? 何を使えば?
• 歌詞の単語数が多すぎて、何が何だかわからない

などなど、細かい壁に何度もぶつかりました。
特にアニメーションの自然な見せ方には、かなりの試行錯誤が必要でした

アニメーションの方法と比較

WordCloudでできないとなった時に、ChatGPTに聞いてみました

方法名 必要環境 動きの自由度 難易度
Streamlit更新型 Python + Streamlit 低〜中(見た目の更新) 簡単
matplotlibアニメ Pythonのみ 中(サイズや色変化)
d3-cloud (JS) ブラウザ + JavaScript 高(自由自在に動かせる) 少し難しい

実際の作業の流れ

で、なにを採用するか?
JavaScript を選択しましたが、ちょっと変更しました
もーっと簡単にしました

1. 形態素解析で単語の頻度を抽出しJSONファイルにする(Python)

✅ポイント

① 出現回数が多い上位30語を抽出する
アニメーションで可視化する際、単語数が多すぎると全体がごちゃごちゃして見づらくなります  そのため、出現頻度の高い単語だけを抜き出します

most_common_words = global_counter.most_common(30)

→ 上位30語を取得

② JavaScriptで使える形式に変換して保存

[["単語1", 出現数1], ["単語2", 出現数2], ...] の形式に変換
JSONファイルとして保存(JavaScriptでWordCloudに使える)

with open("自分のPATHーーここ変更ーー/wordcloud_data.json", "w", encoding="utf-8") as f:
    json.dump(word_data, f, ensure_ascii=False, indent=2)

全体コード①

全体コードはこちら↓
python

import pandas as pd
from janome.tokenizer import Tokenizer
from collections import Counter
import json

# ファイルパス
csv_path = '自分のCSVーここ変更ーー'
df = pd.read_csv(csv_path)

# 形態素解析器
tokenizer = Tokenizer()

# 抽出する品詞
target_pos = ("名詞", "動詞", "形容詞")

# 全体の単語カウント
global_counter = Counter()

# 各行の処理
for _, row in df.iterrows():
    lyric = str(row.get("歌詞", "")).strip()
    if not lyric:
        continue

    words = [
        token.base_form for token in tokenizer.tokenize(lyric)
        if any(token.part_of_speech.startswith(pos) for pos in target_pos)
    ]
    global_counter.update(words)

# 上位N単語を選ぶ(例:上位30語)
most_common_words = global_counter.most_common(30)

# JavaScript用に変換
word_data = [[word, freq] for word, freq in most_common_words]

# JSONとして保存
with open("自分のPATHーーここ変更ーー/wordcloud_data.json", "w", encoding="utf-8") as f:
    json.dump(word_data, f, ensure_ascii=False, indent=2)

print("✅ wordcloud_data.json を出力しました。")

2. VScode内でJSONファイルをJavaScriptで読み込み

✅ポイント

① VScodeのフォルダ構成

nogizaka-wordcloud/
├── index.html
└── wordcloud_data.json

② ローカルサーバーで表示する方法(推奨)
 ローカルで .json ファイルを読み込むには、ローカルサーバーを使う必要があります。以下の方法をおすすめします

方法①:VSCodeの拡張機能「Live Server」を使う(簡単)
左の「拡張機能(四つの四角アイコン)」をクリック
「Live Server」と検索して、Ritwick Dey 作のものをインストール
index.html を開いた状態で右下の「Go Live」をクリック

👉 これでブラウザが自動で開いて、アニメーションが表示されるはずです。

全体コード(仮index.html)

[
  ["Happy", 6],
  ["君", 4],
  ["行く", 6],
  ["どこ", 3],
  ["何", 2]
]

全体コード(wordcloud_data.json)

全体コードはこちら↓
javascript

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>乃木坂46 WordCloud風アニメーション</title>
  <style>
    body {
      font-family: 'Hiragino Maru Gothic Pro', 'Meiryo', sans-serif;
      background-color: #f4f4f4;
      text-align: center;
      margin: 20px;
    }
    h2 {
      font-size: 28px;
      margin-bottom: 8px;
    }
    .search-container {
      margin-bottom: 20px;
    }
    #search-box {
      font-size: 16px;
      padding: 6px 12px;
      width: 250px;
      border: 1px solid #ccc;
      border-radius: 6px;
    }
    canvas {
      border: 1px solid #ccc;
      cursor: default;
    }
  </style>
</head>
<body>

  <h2>乃木坂46</h2>
  <div class="search-container">
    <input type="text" id="search-box" placeholder="単語を検索..." />
  </div>
  <canvas id="cloud" width="360" height="500"></canvas> <!-- 9:16の比率を維持 -->

  <script>
    const searchBox = document.getElementById('search-box');
    const canvas = document.getElementById('cloud');
    const ctx = canvas.getContext('2d');

    let words = [];
    let hoverWord = null;
    let mouseX = 0;
    let mouseY = 0;

    fetch('wordcloud_data.json')
      .then(response => response.json())
      .then(data => {
        words = data.map(([text, freq]) => {
          const size = 20 + freq * 5;
          return {
            text,
            freq,
            size,
            x: Math.random() * (canvas.width - 100) + 50,
            y: Math.random() * (canvas.height - 100) + 50,
            dx: (Math.random() - 0.5) * 0.3,
            dy: (Math.random() - 0.5) * 0.3,
            match: false
          };
        });

        animate();
      });

    searchBox.addEventListener('input', () => {
      const query = searchBox.value.trim();
      words.forEach(w => {
        w.match = query !== "" && w.text.includes(query);
      });
    });

    function animate() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      hoverWord = null;

      for (const w of words) {
        const scale = w.match ? 1.6 : 1;
        const fontSize = w.size * scale;

        w.x += w.dx;
        w.y += w.dy;

        const halfWidth = ctx.measureText(w.text).width / 2;
        const halfHeight = fontSize / 2;

        if (w.x - halfWidth < 0 || w.x + halfWidth > canvas.width) w.dx *= -1;
        if (w.y - halfHeight < 0 || w.y + halfHeight > canvas.height) w.dy *= -1;

        ctx.font = ${fontSize}px 'Hiragino Maru Gothic Pro', 'Meiryo', sans-serif;
        ctx.fillStyle = w.match ? 'red' : hsl(${(w.freq * 30) % 360}, 60%, 40%);
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(w.text, w.x, w.y);

        const textWidth = ctx.measureText(w.text).width;
        if (
          mouseX >= w.x - textWidth / 2 &&
          mouseX <= w.x + textWidth / 2 &&
          mouseY >= w.y - fontSize / 2 &&
          mouseY <= w.y + fontSize / 2
        ) {
          hoverWord = w;
        }
      }

      if (hoverWord) {
        const tipText = ${hoverWord.text}: ${hoverWord.freq};
        ctx.font = "14px sans-serif";
        ctx.fillStyle = "#000";
        ctx.fillText(tipText, mouseX + 15, mouseY + 26);
      }

      requestAnimationFrame(animate);
    }

    canvas.addEventListener('mousemove', e => {
      const rect = canvas.getBoundingClientRect();
      mouseX = e.clientX - rect.left;
      mouseY = e.clientY - rect.top;
    });
  </script>

</body>
</html>  

① fetch('wordcloud_data.json') の使い方(データの読み込み)
 外部ファイル wordcloud_data.json を非同期で読み込んで、中の単語データを words に格納する処理です

fetch('wordcloud_data.json')
  .then(response => response.json())
  .then(data => {
    words = data.map(([text, freq]) => {
      // ...
    });
    animate();
  });

② animate() 関数によるアニメーション制御
 この関数が毎フレーム呼ばれて、単語が動くようになっています
w.x += w.dx のように、各単語をちょっとずつ動かしていて、画面の端に当たったら跳ね返ります

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // ... 各単語の移動、描画、当たり判定など
  requestAnimationFrame(animate);
}

おわりに

最初は「ちょっと動かせたら面白いかも」くらいの軽い気持ちだったのですが、実際に作ってみると、細かい部分でかなりハマりました
少しでも面白いと思っていただけたら嬉しいです✨

今回は 齋藤飛鳥ちゃん推し ということで、あすかちゃんがセンターを務めた以下の4曲を対象にしました:

• ここにはないもの
• 裸足でSummer
• ジコチューで行こう!
• Sing Out!

これらの歌詞を形態素解析し、出現回数が多かった上位30語 をアニメーションで可視化しています👀

1
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
1
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?