GitHubのプロフィールREADME、せっかくならもう少し動かしたくないですか?
自分は、GitHubでの活動を3D惑星として可視化する「GitHub Planet」というWebサービスを作っています。
そして今、自分のGitHubプロフィールにはこんなものを置いています。
自分のGitHub活動から生成された惑星が、そのままGitHubプロフィール上で動いています。
惑星が回る。星が動く。使っている言語によって惑星の見た目も変わる。
今回は、Three.jsで動いているこの惑星をどうやってGIFにして、GitHubプロフィールへ置いたのかを書きます。
そして後半では、
あなた自身のGitHubプロフィールにも、自分の惑星を置く方法
まで紹介します。🪐
GitHub Planetとは
GitHub Planetは、GitHubでの活動を取得して、その人だけの3D惑星を生成するWebサービスです。
GitHubでログインすると、使用言語やContributionなどから自分だけの惑星が生成されます。
単純に言語によって色を変えているだけではなく、言語ごとに惑星で起きる現象も変えています。
例えば、
- CSSなら色が惑星表面を流れる
- C++ならプラズマや放電
- Goなら風
- TypeScriptなら型安全をイメージした外殻
- Rustなら砂や塵
- Kotlinなら紫色の電気
といった感じです。
ログインせずに惑星だけ見たい場合は、Showcaseもあります。
この惑星をGitHubプロフィールにも置きたかった
GitHub Planetには、惑星を横長のカードとして表示するページがあります。
例えば自分の場合は、
https://githubplanet.dev/card.html?username=nitr0yukkuri
です。
ブラウザ上なので、ここではThree.jsが普通に動きます。
惑星が回って、星が動いて、Shaderによる表現も動いています。
これを見ていて思いました。
これ、そのままGitHubプロフィールに置けたらよくない?
GitHub Planetを見るためにサイトへ来てもらうだけではなく、自分のGitHubプロフィールを開いた瞬間に自分の惑星が動いている。
GitHub上の活動から惑星を作るサービスなので、GitHubプロフィールとの相性もかなり良さそうでした。
でもGitHub READMEではThree.jsをそのまま動かせない
ここで問題があります。
GitHubのプロフィールREADMEに、
<script>
// Three.jsを実行
</script>
のようなJavaScriptを置いて、自由に動かすことはできません。
GitHub Planetのカードページをiframeでそのまま埋め込む、という方法も使えません。
つまり、
ブラウザでは動いているThree.jsのカードを、そのままGitHub READMEへ持っていくことはできない。
じゃあ画像にすればいい。
最初はそう考えました。
PNGにすると、一番見せたいものが消える
カードのスクリーンショットをPNGにしてREADMEへ貼るだけなら簡単です。
でもGitHub Planetの場合、静止画にするとかなり大きなものが失われます。
- 惑星が回らない
- 星が動かない
- Shaderの変化が止まる
- 言語ごとの現象が伝わりにくい
例えば、最初に見せたプロフィールカード。
これを一枚だけ切り取っても、惑星の形自体は伝わります。
でも、
GitHub Planetの「生きている感じ」がかなり減る。
それなら、
GitHub README上でThree.jsを動かすのではなく、
動いている状態そのものを画像にすればいいのでは?
と考えました。
そこで使ったのが、
GIFです。
Three.jsをGIF用に作り直すのはやめた
次に考えたのが、
「どうやってThree.jsの惑星をGIFにするか」
でした。
GIF用に別の惑星レンダラーを作ることもできます。
でもGitHub Planetには、すでに完成しているカードページがあります。
card.html
には、
- Three.js
- WebGL
- Shader
- 惑星
- 星
- GitHubのデータ
- 言語ごとのアニメーション
が全部入っています。
だったら、同じものをGIF用にもう一度実装する必要はありません。
完成しているWebページそのものを録画すればいい。
という方針にしました。
Playwrightで本物のカードを録画する
録画にはPlaywrightを使っています。
仕組みはかなり単純です。
GitHub Planetのカードを開く
↓
PlaywrightでChromiumを起動
↓
Three.jsで惑星を描画
↓
数秒間ブラウザを録画
↓
WebM
↓
FFmpeg
↓
GIF
つまり、
Three.js → GIF
と直接変換しているわけではありません。
実際にChromium上でGitHub Planetを動かして、そのブラウザを撮っています。
Playwrightで録画する
録画処理は、ざっくりこんな形です。
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: {
width: 1200,
height: 400
},
recordVideo: {
dir: 'artifacts/video',
size: {
width: 1200,
height: 400
}
}
});
const page = await context.newPage();
await page.goto(
'https://githubplanet.dev/card.html?username=nitr0yukkuri&fix=true'
);
Playwrightにはブラウザを動画として録画する機能があるので、それを使っています。
ここで生成されるのは、まずWebMです。
「5秒待つ」ではなく、惑星が完成するまで待つ
ただし、ページを開いた瞬間に録画を終わらせるわけにはいきません。
GitHub Planetでは、
ページを開く
↓
GitHubのデータを取得
↓
Three.jsを初期化
↓
惑星を作る
↓
言語ごとの表現を適用
という処理があります。
なので単純に、
await page.goto(url);
await page.waitForTimeout(5000);
とするだけだと、通信が遅かった場合に、
まだLOADINGの状態を録画してしまう
可能性があります。
そこで実際には、
await page.waitForFunction(() => {
const canvas =
document.querySelector('#planet-canvas canvas');
const username =
document.querySelector('#username-display')
?.textContent
?.trim();
const language =
document.querySelector('#main-lang-stat')
?.textContent
?.trim();
const planetName =
document.querySelector('#planet-name-sub')
?.textContent
?.trim();
return canvas instanceof HTMLCanvasElement
&& canvas.width > 0
&& canvas.height > 0
&& username
&& username !== 'USERNAME'
&& language
&& language !== 'LOADING'
&& language !== 'UNKNOWN'
&& planetName
&& planetName !== 'UNKNOWN PLANET'
&& planetName !== 'ERROR PLANET';
});
のように、
本当に惑星が表示されたか
を確認してから録画しています。
時間ではなく、状態を待つようにしています。
FFmpegでWebMをGIFにする
Playwrightで録画したら、次はFFmpegです。
WebMをGIFへ変換します。
ざっくり書くと、
ffmpeg \
-i profile-card.webm \
-vf "fps=15,scale=800:266:flags=lanczos" \
-loop 0 \
profile_card.gif
のような処理です。
GitHub Planetでは、GIFの色が崩れにくいようにpalette生成も使っています。
最終的には、
800 × 266
15fps
無限ループ
のGIFとして出力しています。
これで、
GitHub READMEでも惑星が動くようになりました。
READMEでは普通の画像として貼れる
一度GIFになってしまえば、README側では普通の画像と同じです。
例えば、

で表示できます。
クリックしたらGitHub Planetへ飛ばしたいなら、
[](https://githubplanet.dev)
です。
自分のプロフィールでは、
[](https://githubplanet.dev)
という形で使っています。
README上ではThree.jsは一切動いていません。
表示しているのは普通のGIFです。
でも見た目としては、
GitHubプロフィールの中で惑星が動いています。
一回GIFを作っただけでは終わらない
ここでGitHub Planetならではの問題があります。
この惑星は、
GitHub上での活動から作られています。
つまり、自分が開発するとデータも変化します。
コミット数が増える。
Contributionが増える。
使用言語の割合が変わる。
GitHub Planet側の表現自体がアップデートされることもあります。
それなのにプロフィールのGIFだけ昔の状態で止まっていたら、少しもったいない。
そこで、
GIF自体も自動更新する
ことにしました。
GitHub Actionsで自動更新する
GIF生成はGitHub Actionsから実行しています。
流れはこんな感じです。
GitHub Actions
↓
Node.jsを準備
↓
Playwrightを準備
↓
Chromiumをインストール
↓
FFmpegをインストール
↓
最新のGitHub Planetカードを開く
↓
録画
↓
GIFへ変換
↓
GitHub上へ保存
これで、自分で毎回スクリーンショットを撮ったりGIFを作ったりする必要はありません。
GitHubで活動する。
↓
GitHub Planetの惑星が変わる。
↓
Actionsが最新の惑星をGIFにする。
↓
GitHubプロフィールに反映される。
という流れになります。
じゃあGitHub Planet側で全員分のGIFを作ればいいのでは?
ここまで作ると、
「GitHub Planetを使っている全員にGIFを配ればいいのでは?」
となります。
技術的にはできます。
例えば、
/card.html?username=userA
/card.html?username=userB
/card.html?username=userC
を順番に開いて、全部GIFにすればいい。
ただし、このGIFは普通の画像生成とは少し事情が違います。
一人分作るだけでも、
Chromiumを起動
↓
GitHub Planetを開く
↓
GitHubデータを取得
↓
Three.js / WebGLを描画
↓
数秒間録画
↓
WebMを生成
↓
FFmpegでGIFへ変換
↓
GIFを保存
という処理が必要です。
ユーザーが100人なら100回。
1000人なら1000回。
1万人なら1万回。
しかもGitHubの活動は変化するので、一度作って終わりではありません。
毎日最新にしたければ、
毎日全ユーザー分をもう一度レンダリングする
必要があります。
「複数人に対応できない」わけではない
ここは少し大事です。
GitHub Planetが、
複数ユーザーのGIFを技術的に生成できない
わけではありません。
できます。
でも、
GitHub Planet
↓
全ユーザー分Chromium起動
↓
全ユーザー分Three.js描画
↓
全ユーザー分録画
↓
全ユーザー分FFmpeg
↓
全ユーザー分保存
を中央で抱える必要があるのか。
そこには少し疑問がありました。
普通のカードを見るだけならWebページを一度描画すればいいのに、GIFを配るためだけに動画生成処理を全ユーザー分持つことになります。
でも、ここで考え方を変えます。
でも実は、あなたの惑星もGIFにできます
GitHub Planet側で、
全員分を作る必要はありません。
あなたの惑星なら、
あなた自身のGitHub Actionsで、あなた一人分だけ作ればいい。
これなら話が変わります。
GitHub Planetは、
あなたの惑星をWebページとして描画する
ところまで担当する。
あなたのGitHub Actionsは、
そのページを録画
↓
GIFにする
↓
GitHubプロフィールへ表示
を担当する。
つまり、
GitHub Planetが1000人分作る
のではなく、
1000人がそれぞれ自分の1枚だけ作る
という形です。
あなたのGitHubプロフィールにも惑星を置く
ここから実際にやってみます。
まずGitHub Planetを開きます。
GitHubでログインして、自分の惑星が表示されることを確認してください。
ログインする前にどんな惑星があるのか見たい場合はこちら。
GitHub IDが、
octocat
なら、カードURLは、
https://githubplanet.dev/card.html?username=octocat
です。
まずこれを開いて、
自分の惑星カードが表示されること
を確認します。
1. GitHubプロフィール用Repositoryを用意する
GitHubプロフィールREADMEを表示するには、自分のGitHub IDと同じ名前のRepositoryを作ります。
例えばGitHub IDが、
octocat
なら、
octocat/octocat
というRepositoryです。
すでにプロフィールREADMEを使っている人は、そのRepositoryをそのまま使えます。
2. GitHub Actionsを作る
プロフィールRepositoryに、
.github/workflows/update-planet-card.yml
を作ります。
以下は、GitHub Planet本体で使っているGIF生成の仕組みを、プロフィールRepository向けにした例です。
name: Update GitHub Planet Card
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: update-github-planet-card
cancel-in-progress: true
jobs:
generate:
runs-on: ubuntu-latest
steps:
- name: Checkout profile repository
uses: actions/checkout@v4
with:
path: profile
- name: Checkout GitHub Planet
uses: actions/checkout@v4
with:
repository: nitr0yukkuri/githubplanet
path: githubplanet
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: githubplanet/package-lock.json
- name: Install dependencies
working-directory: githubplanet
run: |
npm ci
npm install --no-save --package-lock=false playwright@1.58.2
npx playwright install chromium --with-deps
- name: Install FFmpeg
run: |
sudo apt-get update
sudo apt-get install --yes ffmpeg
- name: Record my planet
working-directory: githubplanet
env:
CARD_RECORD_URL: https://githubplanet.dev/card.html?username=${{ github.repository_owner }}&fix=true
CARD_RECORD_WIDTH: "1200"
CARD_RECORD_HEIGHT: "400"
CARD_RECORD_WARMUP_MS: "2000"
CARD_RECORD_MS: "8000"
run: |
node scripts/record-profile-card.mjs
- name: Convert to GIF
run: |
ffmpeg -y \
-sseof -8 \
-i githubplanet/artifacts/profile-card.webm \
-vf "fps=15,scale=800:266:flags=lanczos,split[frames][palette_source];[palette_source]palettegen=stats_mode=diff[palette];[frames][palette]paletteuse=dither=sierra2_4a:diff_mode=rectangle" \
-loop 0 \
profile_card.gif
- name: Publish GIF
working-directory: profile
run: |
set -euo pipefail
asset_dir="$(mktemp -d)"
cp ../profile_card.gif "$asset_dir/profile_card.gif"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch --orphan generated-planet-card
git rm --recursive --force . || true
cp "$asset_dir/profile_card.gif" .
git add profile_card.gif
git commit -m "Update GitHub Planet card [skip ci]"
git push --force origin HEAD:planet-card-assets
少し長く見えますが、GitHub IDをWorkflowへ直接書く必要はありません。
ここです。
${{ github.repository_owner }}
Repositoryの所有者名をGitHub Actionsから取得しています。
例えば、
octocat/octocat
で実行すれば、
username=octocat
としてGitHub Planetを開きます。
3. 一度Actionsを実行する
GitHubのプロフィールRepositoryから、
Actions
↓
Update GitHub Planet Card
↓
Run workflow
を実行します。
成功すると、
planet-card-assets
というブランチが作られます。
その中に、
profile_card.gif
が生成されます。
4. READMEに惑星を貼る
あとはプロフィールREADMEへGIFを貼るだけです。
[](https://githubplanet.dev)
YOUR_USERNAMEを自分のGitHub IDへ変更します。
例えば、
octocat
なら、
[](https://githubplanet.dev)
です。
これで完成です。
GitHubで活動すると、プロフィールの惑星も変わる
これでGitHubプロフィールを開くと、
自分のGitHub活動から生成された惑星が動いています。
そしてGitHub Actionsを定期実行しているので、惑星のGIFも更新されます。
つまり、
GitHubで開発する
↓
GitHub上の活動が変わる
↓
GitHub Planetの惑星が変わる
↓
GitHub Actionsが新しい惑星を録画する
↓
GitHubプロフィールにも反映される
という流れになります。
普通のプロフィールREADMEは、自分で編集しない限り基本的には同じ見た目です。
でもこのカードなら、
開発を続けることで、プロフィール自体も少しずつ変化していきます。
READMEではThree.jsは動いていない。でも惑星は動いている
今回、最終的にREADMEが表示しているものは普通のGIFです。
でもそのGIFを作る裏側では、
- Three.js
- WebGL
- GLSL
- Playwright
- Chromium
- FFmpeg
- GitHub Actions
を使っています。
GitHub READMEではJavaScriptを自由に動かせない。
だから諦めるのではなく、
動いているWebページそのものを録画して、READMEが扱える形式まで落とす。
という方法を取りました。
READMEではThree.jsは一切動いていません。
それでも見た目としては、
GitHubプロフィールの中で惑星がずっと動いています。
あなたもGitHubに惑星を置いてみませんか
最初は、自分のGitHubプロフィールにGitHub Planetを置きたくて作った仕組みでした。
でも、
GitHub Planet側で全員分のGIFを生成しなくても、
それぞれの人が、自分のGitHub Actionsで自分の惑星だけ作ればいい。
そう考えると、自分だけの機能にする必要はありません。
GitHub Planetは、
あなたのGitHub活動から惑星を作る。
GitHub Actionsは、
その惑星をGIFにする。
GitHub READMEは、
その惑星をプロフィールに表示する。
まずは、自分の惑星を見てみてください。
ログインせずに惑星を見たい場合はこちら。
GitHub Planetのソースコードも公開しています。
もし実際にプロフィールへ惑星を置いてくれた人がいたら、ぜひ見てみたいです。
GitHubプロフィールに、もう少し「動き」を。
あなたのGitHubにも、一つ惑星を置いてみてください。🪐
