Communitygithub.com

walenso/3Dポートフォリオ

3Dヒーロー、各関心ごとに対応したアニメーションオブジェクト、ビジュアルなケーススタディ、動作する問い合わせフォーム(tRPC、Resend、Turnstile、Postgres)を備えたパーソナルポートフォリオサイトを構築し、Vercelにデプロイします。ポートフォリオやパーソナルサイト、特に「3Dポートフォリオ」を求める方で、既成テンプレート以上の差別化を望む場合に使用します。

3Dポートフォリオ とは?

このスキルは、単なるテンプレートを超えた、その人だけのオリジナルな3Dポートフォリオサイトを構築するためのものです。3Dオブジェクトを活用したヒーローセクションや、各プロジェクトや趣味を表現するアニメーションオブジェクトを特徴とし、視覚的に印象的なサイトを作成します。スキルはVite、React、TypeScript、three.jsをベースに、バックエンドにtRPC、Zod、Resend、Cloudflare Turnstile、Neon Postgresを使用し、Vercelへのデプロイを想定しています。コンテンツは一つの型付きファイルに集約され、ユーザーがコンポーネントに触れずにテキストを編集できるよう設計されています。また、モーションの無効化、メール配信のテストモード、スパム対策など、実運用を考慮した細かな設定が含まれており、デザインからデプロイ、テストまでをカバーする包括的なスキルです。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/walenso/3d-portfolio-skill/tree/HEAD/skills/3d-portfolio

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

3Dポートフォリオ は何をしますか?

この人にしか作れない、ワンページのポートフォリオを構築しましょう。このスキルを生み出したサイトは、DJのレコードケースをモチーフに作られました。各プロジェクトはめくるレコードジャケットで、手前にあるレコードは回転します。例: https://alessio-simoya.vercel.app

そのコンセプトをコピーしてはいけません。プロセスに従い、その人自身のコンセプトを見つけてください。

1. まず入力を収集する

デザインの前にこれらを尋ねてください。最初から実際のコンテンツを基に構築します。

  • プロジェクト: 各プロジェクトについて、概要、自分の役割、日付、成果、使用スタック、公開リンク、そして主張してはいけないことのリストを簡潔に。
  • 職歴: 会社名、役職、期間、場所、各職場でのひとつの成果。
  • スキル: グループ化し、それぞれをどこで証明したか。
  • 仕事外の活動: 趣味、チーム、料理、訪れた場所。ここからコンセプトが生まれます。相手が提供する以上の情報を引き出しましょう。
  • アセット: ロゴ、製品スクリーンショット、動画、RiveまたはLottieファイル。プロジェクトフォルダがどこにあるか尋ね、検索してください。
  • リンク: LinkedIn、GitHubなど。特に許可がない限り、メールアドレスを公開してはいけません。

2. ひとつのコンセプトを見つける

その人の世界で最も特徴的なものを選び、それをヒーローにします。触って遊べる本物の3Dオブジェクトとして表現します。DJにはレコードのケース、クライマーにはホールドの壁、パン屋にはパンのトレイ。各プロジェクトはそのオブジェクトの中の一品になります。

その大胆さに集中しましょう。ページの他の部分は落ち着いたものにします。フラットなカラーフィールド、個性のある表示用書体、シンプルな本文用書体、四つか五つの名前付きカラー。

3. スタック

  • Vite、React、TypeScript(strictモード)
  • three.jsを直接使用(シーンフレームワーク不要)
  • Riveは、その人が既に.rivファイルを持っている場合のみ使用。wasmは遅延ロード
  • APIにtRPCとzod、Vercel Functionとしてapi/trpc/[trpc].tsに配置
  • メールにResend、スパム対策にCloudflare Turnstile、レコード保存にNeon Postgres
  • セルフホストの可変フォントを@fontsource-variable/*で使用

4. 3Dの構成方法

ひとつの固定フルスクリーンWebGLキャンバスをpointer-events: noneで使用し、すべての3DビューをDOMプレースホルダーの矩形領域に描画します。

const r = el.getBoundingClientRect();
renderer.setViewport(r.left, innerHeight - r.bottom, r.width, r.height);
renderer.setScissor(r.left, innerHeight - r.bottom, r.width, r.height);
renderer.render(scene, camera);

フレームごとに一度クリアし、シザーテストを有効にし、画面外の要素はスキップします。これにより、ひとつのコンテキストでページ内に複数の3Dオブジェクトを配置でき、レイアウトに合わせてスクロールします。

各小さなオブジェクトに「リグ」を与えます。グループ、カメラ位置、注視点、そしてひとつの共有クロックで駆動するtick(t)関数です。リグは遅延構築し、一度にひとつだけ表示します。テクスチャは2Dキャンバスに描画します(ロゴ、ラベル、パターンなど)。これにより、読み込むモデルファイルは不要です。

うまくいかなかった点とその回避方法:

  • 要素より大きな地面影の平面は、硬い矩形として表示されます。ビュー内に収まるようにサイズ調整してください。
  • 「自身の軸で回転」させたい場合は、グループを回転させ、その子を回転させます。
  • スプライトと透明平面は、ソリッドメッシュの後に描画されます。オブジェクトの下にあるグローは、カメラ向きスプライトではなく、その下に配置したフラットな平面にします。

5. 機能したセクション

  1. ヒーロー: 名前を大きく表示し、役割を循環表示するステッカー、ひとつの文、インタラクティブなオブジェクト。背後にある色付きの円盤は、フォーカスしているアイテムに応じて変化します。
  2. 作品: 各ケーススタディは、そのブランドカラーのバンドと実際のロゴで始まり、画像、動画、アニメーションの横スクロールリール、3つの短いハイライト、いくつかの大きな数値、スタックチップが続きます。長文は「すべてのストーリーを読む」の背後に配置します。
  3. 職歴: 各職場を一行で、大きな数字を一つずつ表示します。
  4. 経歴: 学校やマイルストーンを、それぞれアニメーションオブジェクトとともに表示します。
  5. スキル: テーブルではなく、動くタイプの行として表示します。
  6. 仕事外の活動: 各関心事にアニメーションオブジェクトをひとつずつ表示します。
  7. 旅: 訪問した場所を、スチールドアに貼られた冷蔵庫マグネットとして表示し、ドラッグ可能にします。
  8. お問い合わせ: 問い合わせフォーム。

フローティングピルヘッダーがIntersectionObserverで現在のセクションをマークします。

6. ルール

  • 事実のみ 提供された情報のみを使用します。セクションに必要な情報がない場合は、そのセクションを省略します。各プロジェクトの「主張してはいけないこと」リストは、コンテンツファイルにコメントとして残します。
  • 無駄なカウントはしない コミット数、コード行数、ページ数やモデル数は未熟に見えます。プロダクトとその成果を示しましょう。
  • テキストを減らす 画面が段落で埋め尽くされている場合は、ビジュアルに変換します。
  • 他者の作品を尊重する 有料テンプレートをプレビューから再構築したり、ブランドのプロダクトデザイン(例:特定の試合球)をコピーしてはいけません。同じ精神でオリジナルを描きましょう。自由にライセンスされた画像のみを使用し、ページ上でクレジットを表示します。
  • スクリーンショット 実際の顧客名、金額、顔を表示してはいけません。
  • モーションにはオフスイッチ prefers-reduced-motionを尊重し、「モーションを一時停止」ボタンを追加し、WebGLがない場合のフラットなフォールバックを提供します。
  • すべてのコンテンツをひとつの型付きファイルに src/content/portfolio.ts に集約し、ユーザーがコンポーネントに触れずにテキストを編集できるようにします。

7. 問い合わせフォーム

ひとつのtRPCプロシージャ contact.send を、zod入力とともに使用します。順に以下の処理を行います:

  1. 隠しハニーポットフィールドが入力されていれば、成功を黙って返します。
  2. アドレスごとの小さなレート制限を適用します。
  3. TurnstileトークンをCloudflareで検証し、失敗した場合は拒否します。
  4. Resendを通じてオーナーにメールを送信し、送信者をreply_toに設定します。
  5. すべてのメッセージをmessagesテーブルに保存します。
  6. 訪問者がデフォルトでオフの「最新情報を受け取る」チェックボックスをオンにした場合のみ、同意文言と時間とともにsubscribersテーブルに保存し、Resendのオーディエンスにも追加します。

そのチェックなしに、誰かをメーリングリストに追加してはいけません。

環境変数: RESEND_API_KEY, ENQUIRY_TO_EMAIL, ENQUIRY_FROM_EMAIL, RESEND_AUDIENCE_ID, DATABASE_URL, TURNSTILE_SECRET、およびブラウザ用の公開サイトキー。

うまくいかなかった点:

  • from の値が既に 名前 <アドレス> の形式になっている場合があります。これをさらにラップすると、Resendが422を返します。< が含まれている場合はそのまま使用します。
  • Viteは envPrefix で指定されたプレフィックスで始まる変数のみを公開します。サイトキーが別の名前で渡される場合は、そのプレフィックスを追加します。
  • Turnstileウィジェットは、Cloudflareで許可されたホスト名でのみ動作します。
  • 本番環境でTurnstileのシークレットがない場合は、送信を拒否します。Cloudflareの公開テストキーはローカル開発でのみ使用します。
  • CONTACT_DRY_RUN=1 スイッチを追加します。本番環境では無視され、メールを送信せずに全パスをテストできます。
  • vite dev で同じハンドラーを実行するには、関数を server.ssrLoadModule で読み込み、Request を渡す小さなプラグインを追加します。
  • メールHTMLはテーブルとインラインスタイルで構築し、すべてのフィールドをエスケープします。

8. 作業の都度チェック

変更のたびに:

  • ビルドを実行します。
  • Playwrightで各セクションのスクリーンショットを1440pxと390pxの幅で撮影します(Chromiumを --use-gl=swiftshader で起動し、WebGLがヘッドレスでレンダリングされるようにします)。
  • document.documentElement.scrollWidth - innerWidth が0であることを確認します。
  • 完了と言う前に、自分でスクリーンショットを確認します。

確認しなかったことを明確に述べます。実機のスマートフォン、Safari、モーションのフィーリング、実際のメール配信。

9. 公開

  • git init し、.env* と .vercel を無視リストに追加。最初のコミットの前に、ステージングされたファイルにキーがないかスキャンします。
  • vercel link を実行し、環境変数を追加し、vercel --prod を実行します。
  • 公開ページが失敗したリクエストなく読み込まれ、APIがTurnstileトークンなしのリクエストを拒否することを確認します。
  • オーナーに実際のメッセージをライブフォームから送信してもらいます。

スタータープロンプト

この人に以下の入力を依頼します:

3d-portfolioスキルを使って、私の3Dポートフォリオを構築してください。

名前と一言説明:
ヒーローで循環表示する役割(4~6個):
プロジェクト(各プロジェクトの概要、役割、日付、リンク、主張してはいけないこと):
職歴(会社名、役職、期間、それぞれのひとつの成果):
スキル(グループ化して):
学校やマイルストーン:
仕事外の関心事(具体的に):
訪れた場所:
ロゴ、スクリーンショット、アニメーションの保存場所(このマシン上で):
表示するリンク(LinkedIn、GitHubなど):
サイトに絶対に載せたくないもの:

関連スキル