はじめに
このたび、文字列を入力するとデータ構造である Trie 木(トライ木) がリアルタイムで 3D の盆栽として育っていく Web アプリ「Trie Bonsai」をリリースしましたので、その紹介と技術的なこだわりについて記事を書きました。
リンク
ログイン不要で、その場で文字を入力するだけで盆栽が育ちます。まずは触ってみてください
アプリ作成のきっかけ
大学のアルゴリズムの授業で「Trie 木」というデータ構造を初めて学んだとき、教科書に載っていた図を見て一つの違和感を覚えました。
「これ、植物の枝分かれにそっくりじゃないか?」
Trie 木は文字列の共通接頭辞(プレフィックス)を枝として共有する木構造で、辞書検索やオートコンプリートで広く使われています。"car", "cat", "card" を入れれば "c → a → r / t / r→d" と枝分かれしていく――この性質は、まさに育っていく植物のそれと重なります。
しかし、Web で Trie 木を検索しても出てくるのは「無味乾燥な丸と線の図」ばかり。
「データ構造を、教科書の中の抽象的な絵ではなく、手のひらに置きたくなる盆栽として表現したい」――そんな思いが、このアプリを作る出発点になりました。
サービスの思想と目標
Trie Bonsai が目指したのは、次の 3 つです。
-
データ構造の "美しさ" を直感的に味わえる体験を作る
アルゴリズムの教科書を一行も読まなくても、「ああ、文字を入れるたびに枝が分かれていくんだな」と肌感覚で理解できる UI を目指しました。 -
触っていて気持ちのいいインタラクション
3D 空間をぐるぐる回したり、好きな角度で眺めたり。データ構造を「鑑賞対象」として扱える Web アプリにしたかったのです。 -
エッジ環境だけで本番運用に耐えるプロダクトを一人で組み上げる
技術面では「Cloudflare のエッジスタック(Pages / Workers / D1 / R2)だけでフロントからストレージまで完結させる」ことを学習目標に設定しました。
サービス概要
トップページ
「文字列から生まれる、美しい盆栽。」をキャッチコピーに、背景にサンプル盆栽が静かに育っているランディングページです。ログインなどの手続きを挟まず、ワンクリックで作成画面に遷移できます。
作成画面
文字列を入力して「生成」を押すと、入力された単語群から構築される Trie 木が、3D 盆栽として背面にリアルタイム描画されます。マウスドラッグで自由に視点を変えられるので、上から、横から、斜めから――好きな角度で鑑賞できます。
設定画面
「設定」パネルからは、以下の要素を切り替えられます。
- 木の種類:プレフィックス木 / Patricia 木 / Suffix 木
- ノードのグラデーション:Dusty Grass / New Life / Blessing / mochiHoppe など
- 背景タイプ:雪 / 夜明 / 単色
各項目には、データ構造の違いや配色のコンセプトを説明するモーダルも完備しています。「Patricia 木って何?」と思ったらその場で学べる、というのもこだわりポイントの 1 つです。
画像保存モーダル
気に入った盆栽は、PNG 画像としてローカルにダウンロードできます。Canvas を toDataURL() でスナップショット化する仕組みです。
ギャラリー画面
他のユーザーが公開した盆栽作品を一覧表示できます。作品名で検索でき、カードをクリックすると個別の作品詳細ページへ遷移します。
サムネイル画像は Cloudflare R2 から CDN 経由で直接配信 されるため、Workers を経由せず高速に表示されます(後述)。
ギャラリーへの投稿
作品にタイトルを付けて公開すると、画像は R2、メタデータは D1 へ 並列保存 され、個別 URL でいつでも鑑賞できるようになります。
動作イメージ
文字を入力するたび、共通接頭辞ごとに枝分かれしながらリアルタイムに盆栽が成長していきます。
使用技術
フロントエンド
| Category | Technology |
|---|---|
| 言語 | TypeScript 5 |
| フレームワーク | Next.js 16 (App Router) |
| UI ライブラリ | React 19 |
| 3D 描画 | Three.js, React Three Fiber, Drei, Postprocessing |
| GUI コントロール | Leva |
| 状態管理 | Zustand |
| スタイリング | Tailwind CSS v4 |
バックエンド / インフラ
| Category | Technology |
|---|---|
| 言語 | TypeScript |
| API フレームワーク | Hono (Cloudflare Workers 上で稼働) |
| ORM | Drizzle ORM |
| データベース | Cloudflare D1 (SQLite 互換) |
| オブジェクトストレージ | Cloudflare R2 (S3 互換、パブリック CDN 配信) |
| フロントエンドホスティング | Cloudflare Pages |
| デプロイツール | Wrangler |
| CI / CD | GitHub 連携による自動デプロイ |
開発ツール
| Category | Tool |
|---|---|
| Linter | ESLint |
| Formatter | Prettier |
| マイグレーション | Drizzle Kit |
選定のポイントは、「Cloudflare のエッジスタックだけで完結させる」 ことでした。フロントもAPIもDBも画像配信も、全部 Cloudflare の中に閉じています。これにより、低遅延・低運用コストを両立できました。
システム構成
エッジ側で全ての処理を完結させることで、低遅延と低運用コストを両立しています。
ギャラリーの画像は Workers を経由せず R2 から直接 CDN 配信されるため、Egress 課金も発生しません。
ER 図と設計の説明
Cloudflare D1 では Single Table Design を採用し、構造データ(入力単語リスト)と表示設定(配色やテーマ)を JSON カラムにまとめることで正規化を省略しています。
これは、本アプリのデータが「1 作品 = 1 レコード」で完結し、リレーションを跨いだ複雑なクエリが発生しないと判断したためです。D1 のような SQLite 互換 DB では JSON 型を素直に扱えるので、表示設定が増えてもスキーマ変更なしで対応できるのがメリットでした。
こだわりポイント
1. 再帰的な放射状レイアウトで「盆栽らしさ」を出す
普通に Trie 木を 3D 化すると、上から下へ伸びる「家系図」のような見た目になります。これでは盆栽になりません。
そこで、親ノードを中心に子ノードを放射状に配置し、深さに応じて枝の角度を少しずつ傾ける という再帰的なレイアウトアルゴリズムを実装しました。これによって、自然な「枝振り」が生まれ、データ構造でありながら有機的なシルエットが得られています。
2. Cloudflare R2 から CDN 直接配信で Egress ゼロ
ギャラリーで他のユーザーの作品を一覧する際、画像をすべて Workers 経由で配信すると、Workers の CPU 時間を浪費し、レイテンシも増えます。
そこで、R2 を パブリックバケット として公開し、ブラウザから R2 の CDN に直接アクセスする構成にしました。これにより、Workers は API ロジック(メタデータの保存・取得)にだけ集中でき、画像配信は Cloudflare のグローバル CDN に任せられます。Egress 料金もかかりません。
3. 画像とメタデータを「並列保存」する投稿フロー
作品を公開する際は、PNG 画像(R2)とメタデータ(D1)を保存する必要があります。これを直列で行うとレスポンスが遅くなるため、Promise.all で並列に発行 し、両方が成功したら投稿完了とする設計にしました。
4. データ構造を「学べる」ヘルプモーダル
設定画面で「Patricia 木」「Suffix 木」を切り替えられる際、ユーザーが「これ何?」と思ったその場で学べるよう、各項目に解説モーダルを用意しています。アプリで遊びながらアルゴリズムにも触れられる、というのが個人的にお気に入りのポイントです。
今後の展望
MVP として 8 フェーズの開発を完了し、現在は本番運用中です。今後は 鑑賞体験と表現力の拡張 を進めていきます。
-
次フェーズ
- パトリシア木・サフィックス木の切り替え UI のブラッシュアップ
- テーマカラー(季節)プリセットの追加
- 作品への簡易リアクション機能
-
長期構想
- ユーザーアカウント機能
- 自分だけのコレクション機能
- Web Share API による画像共有強化
おわりに
ここまで読んでいただきありがとうございました。
「データ構造を盆栽にする」というアイデアそのものは小さなものですが、Cloudflare のエッジスタックを一通り触り、3D 表現と向き合うことで、個人開発として得るものはとても大きかったです。
もし少しでも興味を持っていただけたら、ぜひ一度触ってみてください。文字を入れるたびに枝が伸びていく様子は、見ているだけでちょっと癒されると思います。
ご感想・フィードバックお待ちしております。






