GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

css 3d アニメーション で作る マウス追従の立体シーン9選

css 3d アニメーション は、マウスの位置を角度に変えて観覧車や地形、橋のミニチュアを回す演出です。Three.jsなしで、キャンバス2D の上に座標計算だけで組み立てました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つの並び順は派手さではなく、遠近計算が積み重なる順番で決めています。まず点ひとつを回して画面に投影する最も単純な形が土台になり(01・02)、そこに回転行列と群れの規則が重なって複数の点が一緒に動きます(03・04)。続いて格子や螺旋のように点が曲面上の規則に従い(05・06)、最後に物理と波が場面全体の流れを作ります(07〜09)。css 3d perspective を手計算で再現しているという点は、9項目すべてに共通しています。

01観覧車シーン

見えるもの

遊園地・イベント紹介のランディングヒーローに使う、16本のスポークが遠近で近づいてくる回転観覧車のミニチュアです。

観覧車シーン — 遠近で近づく16本のスポーク
新しいタブで開く

01 · 観覧車シーン — 遠近で近づく16本のスポーク — 計測 累積面積 23.298% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotate3dsortrequestAnimationFrame

二回転で生まれる奥行き

function wheelPoint(theta: number, radius: number): Pt3 {
  const x = Math.cos(theta) * radius;
  const y = Math.sin(theta) * radius;
  const ct = Math.cos(TILT), st = Math.sin(TILT);          // x軸の傾き — ホイールを後ろに倒す
  const y1 = y * ct, z1 = y * st;
  const ca = Math.cos(YAW), sa = Math.sin(YAW);            // y軸のひねり — 円盤が正面を向かないようにする
  return { x: x * ca + z1 * sa, y: y1, z: -x * sa + z1 * ca };
}

function project(p: Pt3): Proj {                          // 遠近投影 — この一行がすべて
  const size = sizeRef.current;
  const s = focalRef.current / (focalRef.current + p.z);
  return { sx: size.w * 0.5 + p.x * s, sy: size.h * 0.56 - p.y * s, s };
}

円周上の点をx軸で一度倒し、y軸でもう一度ひねって初めて平らな円盤がホイールらしく見えます。焦点距離をz値で割る一行が近いゴンドラを大きく描く仕組みです(実測: 累積面積23.298パーセント、最大フレーム変化11.328パーセント、23/24フレームで動きました)。

02ハイトフィールド地形

見えるもの

ゲーム・地図サービスの紹介に使う、格子点の高さを2つのサイン波で計算し、マウス角度で回るさざ波の丘です。

ハイトフィールド地形 — マウスで回るさざ波の丘
新しいタブで開く

02 · ハイトフィールド地形 — マウスで回るさざ波の丘 — 計測 累積面積 28.832% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

focalsinbeginPath

三つの波が丘を作る

function height(i: number, j: number, t: number): number {
  const x = (i / (COLS - 1)) * TAU;      // 0..TAU
  const y = (j / (ROWS - 1)) * TAU;
  return Math.sin(x * F1 + t) * 0.62 + Math.sin(y * F2 + t * 0.8) * 0.38
       + Math.sin((x + y) * 0.9 + t * 1.3) * 0.18;
}

function corner(i: number, j: number, t: number): Corner {
  const size = sizeRef.current;
  const heading = headingRef.current, pitch = pitchRef.current, amp = ampRef.current;
  const wx = (i / (COLS - 1) - 0.5) * 430;
  const wz = (j / (ROWS - 1) - 0.5) * 340;
  const wy = height(i, j, t) * amp;
  const ch = Math.cos(heading), sh = Math.sin(heading);
  const x1 = wx * ch - wz * sh;
  const z1 = wx * sh + wz * ch;
  const a = ALT + pitch;
  const ca = Math.cos(a), sa = Math.sin(a);
  const y2 = wy * ca - z1 * sa;
  const z2 = wy * sa + z1 * ca;
  const s = FOCAL / (FOCAL + z2 + 300);
  return { sx: size.w * 0.5 + x1 * s, sy: size.h * 0.62 - y2 * s, s, h: wy };
}

格子点ごとに3つのサイン波を足して高さを決め、その高さをカメラの高度角でもう一度回すことで、波が横に流れるのではなく山の尾根が寝返るように見えます(実測: 累積面積28.832パーセント、23/24フレームで動きました)。

03分子模型シーン

見えるもの

科学・教育サービスの紹介に使う、原子15個が回転行列で回りながら結合していく分子模型です。

分子模型シーン — 原子が結合する場面
新しいタブで開く

03 · 分子模型シーン — 原子が結合する場面 — 計測 累積面積 10.773% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

projectsortlineTo

回転行列は二行で足りる

function rot(p: Pt3): Pt3 {
  const cy = Math.cos(angYRef.current), sy = Math.sin(angYRef.current);
  const x1 = p.x * cy + p.z * sy;
  const z1 = -p.x * sy + p.z * cy;
  const cx = Math.cos(angXRef.current), sx = Math.sin(angXRef.current);
  const y2 = p.y * cx - z1 * sx;
  const z2 = p.y * sx + z1 * cx;
  return { x: x1, y: y2, z: z2 };
}

function project(p: Pt3): Proj {
  const size = sizeRef.current;
  const s = FOCAL / (FOCAL + p.z + 150);      // 遠近投影 — この一行がすべて(近いほど大きく)
  return { sx: size.w * 0.5 + p.x * s, sy: size.h * 0.5 - p.y * s, s };
}

y軸回転を一度、x軸回転をもう一度つなげるだけで任意の方向に回せます。原子ごとに半径を±5パーセントの脈動で揺らすと、結合が呼吸しているように見える工夫です(実測: 累積面積10.773パーセント、23/24フレームで動きました)。

04鳥の群れシーン

見えるもの

自然・環境キャンペーンページに使う、分離・整列・結合の3つのルールで40羽が群れを保つシーンです。

鳥の群れシーン — 3つのルールで保つ群れ
新しいタブで開く

04 · 鳥の群れシーン — 3つのルールで保つ群れ — 計測 累積面積 28.538% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

requestAnimationFramesqrtsort

三つのルールが一フレームで混ざる

if (cn > 0) {
  ax += (cxs / cn - b.x) * coh * 1.1; ay += (cys / cn - b.y) * coh * 1.1; az += (czs / cn - b.z) * coh * 1.1;  // 結合
  ax += (vxs / cn - b.vx) * 1.1; ay += (vys / cn - b.vy) * 1.1; az += (vzs / cn - b.vz) * 1.1;                  // 整列
}
ax += sxs * 260; ay += sys * 260; az += szs * 260;                                                              // 分離
ax += (st.food.x - b.x) * FOOD_GAIN * coh; ay += (st.food.y - b.y) * FOOD_GAIN * coh; az += (st.food.z - b.z) * FOOD_GAIN * coh;
b.vx += ax * dt; b.vy += ay * dt; b.vz += az * dt;
const sp = Math.sqrt(b.vx * b.vx + b.vy * b.vy + b.vz * b.vz) || 1;
const cl = sp > SPD_MAX ? SPD_MAX / sp : (sp < SPD_MIN ? SPD_MIN / sp : 1);
b.vx *= cl; b.vy *= cl; b.vz *= cl;

近くの鳥の平均位置・平均速度と、近すぎる鳥を押し返す力を毎フレーム足したあと速度を上限と下限で挟むことで、群れが一塊に固まりすぎず散らばりすぎもしません(実測: 累積面積28.538パーセント、最大フレーム変化9.444パーセント)。

05トーラス格子

見えるもの

製品・テックブランドのヒーローに使う、トーラス格子点に沿って波の位相が流れる光の結晶シーンです。

トーラス格子 — ドーナツ格子上を流れる光
新しいタブで開く

05 · トーラス格子 — ドーナツ格子上を流れる光 — 計測 累積面積 29.176% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

cossinfillStyle

波がチューブ半径そのものを揺らす

function tubeR(u: number, v: number, tube: number, now: number): number {
  const ph = (u / TAU) * 2 + (v / TAU) * 3 + now / 1300;
  return tube + Math.sin(ph * TAU) * WAVE_GAIN;
}

function torusPoint(u: number, v: number, tube: number, now: number): P3 {
  const rr = tubeR(u, v, tube, now);
  const ring = 112 + Math.cos(v) * rr;
  return { x: ring * Math.cos(u), y: Math.sin(v) * rr, z: ring * Math.sin(u) };
}

チューブ半径そのものを時間で波打つ値に変えると、表面は固定されたドーナツではなく光が通り過ぎる結晶のように明るさが移動して見えます(実測: 累積面積29.176パーセント、最大フレーム変化25.398パーセント — 9項目中もっとも広く動きました)。

06螺旋階段シーン

見えるもの

建築・空間紹介ページに使う、踊り場26段を角度と高さで積み上げて見上げる螺旋階段のシーンです。

螺旋階段シーン — 上りながら回る踊り場
新しいタブで開く

06 · 螺旋階段シーン — 上りながら回る踊り場 — 計測 累積面積 24.083% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotate3dsortarc

黄金角で踊り場をずらす

function treadCorners(i: number, rise: number): { inA: P3; inB: P3; outA: P3; outB: P3 } {
  const st = stateRef.current;
  const ang = i * GOLD + st.spin;
  const y = i * rise * RISE_K;
  const wide = 0.9 - i / STEPS * 0.42;    // 低い段ほど視界を広く占める角幅
  const a0 = ang - wide, a1 = ang + wide;
  return {
    inA: { x: Math.cos(a0) * RIN, y: y, z: Math.sin(a0) * RIN },
    inB: { x: Math.cos(a1) * RIN, y: y, z: Math.sin(a1) * RIN },
    outA: { x: Math.cos(a0) * ROUT, y: y, z: Math.sin(a0) * ROUT },
    outB: { x: Math.cos(a1) * ROUT, y: y, z: Math.sin(a1) * ROUT }
  };
}

踊り場ごとに角度を黄金角だけずらして積むと層が重なって見えません。低い段ほど角幅を広げることで、見上げたときの遠近が自然に狭まる効果も得られます(実測: 累積面積24.083パーセント、23/24フレームで動きました)。

07ジェットコースター軌道

見えるもの

ゲーム・体験型キャンペーンに使う、曲線の軌道に沿って光の玉が加速・減速しながら走るシーンです。

ジェットコースター軌道 — 曲線を走る光の玉
新しいタブで開く

07 · ジェットコースター軌道 — 曲線を走る光の玉 — 計測 累積面積 18.396% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

quadraticCurveToarcsort

傾きがそのまま加速度になる

function step(dt: number): void {
  const st = stateRef.current;
  const track = trackRef.current, cum = cumRef.current;
  const p0 = track[st.segIdx], p1 = track[(st.segIdx + 1) % SAMPLES];
  const segLen = cum[st.segIdx + 1] - cum[st.segIdx];
  const slope = (p1.y - p0.y) / segLen;              // 下り(−)で加速
  const g = gravValRef.current * 2;                  // ダイヤル% → px/s²
  st.beadV += -g * slope * dt;
  st.beadV *= 1 - 0.05 * dt;
  if (st.beadV < VMIN) st.beadV = VMIN;
  st.beadS += st.beadV * dt;
}

軌道上2点の高低差を区間の長さで割った傾きに重力値を掛けて速度に足すだけで、上りで遅くなり下りで速くなる動きがアニメーションではなく計算から自然に生まれます(実測: 累積面積18.396パーセント、最大フレーム変化6.998パーセント)。

08浮遊島シーン

見えるもの

ファンタジー・ゲームのランディングに使う、島7つがそれぞれ違う位相で波の上に浮かぶシーンです。

浮遊島シーン — 波の上に浮かぶ島々
新しいタブで開く

08 · 浮遊島シーン — 波の上に浮かぶ島々 — 計測 累積面積 8.134% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translate3dscalesin

島ごとに位相がずれてこそ浮く

function project(p: P3): Proj {
  const st = stateRef.current;
  const s = 320 / (320 + p.z + 40);   // FOCAL=320, BACK=40
  return { sx: st.W * 0.5 + p.x * s, sy: st.H * 0.52 - p.y * s, s: s };
}

function islandWorld(isl: { baseY: number; phase: number; speedK: number; ang: number; r: number }, now: number): P3 {
  const amp = ampValRef.current;
  const by = isl.baseY + Math.sin(now / 1000 * FLOAT_SPEED * isl.speedK + isl.phase) * amp;
  return { x: Math.cos(isl.ang) * isl.r, y: by, z: Math.sin(isl.ang) * isl.r };
}

極座標で配置した島ごとに異なる位相値をサイン関数に入れることで、同じリズムで一斉に上下せず、島ごとにばらばらに浮いているように見えます(実測: 累積面積8.134パーセント、最大フレーム変化1.866パーセント)。

09アーチ橋シーン

見えるもの

インフラ・都市紹介ページに使う、アーチの区間をサイン曲線で計算し、マウス角度でなぞる橋のシーンです。

アーチ橋シーン — マウス角度でなぞる橋
新しいタブで開く

09 · アーチ橋シーン — マウス角度でなぞる橋 — 計測 累積面積 4.622% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotate3dsinlineTo

サイン一つで橋の曲線ができる

function archY(t: number, height: number): number { return Math.sin(t * Math.PI) * height; }
function archX(t: number): number { return (t - 0.5) * SPAN; }

function project(p: P3): Proj {
  const st = stateRef.current;
  const s = 300 / (300 + p.z + 90);   // FOCAL=300, BACK=90
  return { sx: st.W * 0.5 + p.x * s, sy: st.H * 0.62 - p.y * s, s: s };
}

橋の進行率t(0から1まで)をサイン曲線ひとつに入れると、両端で0、中央で最大高さになるアーチ形状がそのまま得られます。石ブロック22個をこの曲線上にz順で積むだけで橋が完成します(実測: 累積面積4.622パーセント、最大フレーム変化1.909パーセントで9項目中もっとも小さく動きましたが、キーフレーム0個でも静止カードではありません)。

どこで壊れるか — 落とし穴

回転が一回だと平たい楕円になる

もっとも多い落とし穴は、回転を一回しかさせないことです。01観覧車のように円盤ひとつをx軸だけで倒すと画面上ではまだ平たい楕円のままです。y軸回転を加えて円盤がカメラを正面から見ないようにひねって初めて、css 3d transform と呼べる立体感が生まれます。04鳥の群れのように複数の点が一緒に動くシーンでは順序の問題も付いてきます。z値でソートせず配列の順のまま描くと、奥にいるはずの鳥が手前の鳥を覆ってしまいます。

z ソートがないとブロックが橋を突き抜ける

09アーチ橋は最初、石ブロックをzソートせずに配列の先頭から描いていましたが、マウスで角度を回すと奥のブロックが時々手前を突き抜けて見えました。毎フレームブロックのz値を計算し直してソートしてから、画家アルゴリズムの通り遠いものから塗ることで重なりが消えました。

波の振幅が大きすぎると面が内側に折れる

05トーラスのように css アニメーション 回転 3d を手計算で再現する波の値が大きくなりすぎると、チューブ半径が負に反転して面が内側に折れ込むのも実測中に見たエラーです。9つのシーンの完成ソースとReact版は本文下の暗号zipに入っていて、開く暗号はafcdc6hjです。

アクセシビリティ

すべてのシーンは role="application" と tabindex="0" を持つキャンバスの上で動作し、フォーカスが来るとアウトラインで示されます。prefers-reduced-motion: reduce が有効な場合、01・05・08・09は最後に見えていた角度のまま一枚だけ描いて回転を止めます。02地形は波がもっとも読み取りやすい固定時刻(0.6)の一枚で止まり、03分子はポインターを動かしたその瞬間の角度で固定されます。06階段と07軌道、04群れも同じ方式で、要求があればrequestAnimationFrameループを回さずに静止画一枚だけを描きます。詳しい値はWCAGの動作軽減の解説を参照してください。

FAQ

css 3d perspective を使わずにこの立体シーンを作ったのですか?

はい、CSSのperspectiveプロパティではなく、キャンバス2Dの上で座標を直接回転・投影する計算で作りました。focal / (focal + z)という一行が遠近縮小の役割を果たし、その値にx・yを掛けて画面座標を求めます。

9つのシーンはすべてマウスだけで操作しますか?

はい、キャンバス上でポインターのx・y比率を読み取り、回転速度やカメラの傾きに変換しています。タッチ画面では指の座標を同じ形で受け取り、同じように動作します。

css アニメーション 回転 3d のコードをそのまま自分のプロジェクトに貼ってもいいですか?

はい、ライブラリやCDNへの依存なしに純粋なキャンバス計算だけを使っているので、各項目のコードブロックをそのまま移せば動作します。zipの中にはindex.html・style.scssの原本とReact移植版が一緒に入っています。

関連してキャンバス特殊効果9種も同じ方式のピクセル計算を扱っています。DOMトランスフォームで回るマウスに追従するCSS 3D回転シーン、球面座標を扱う3D地球儀キャンバスシーンも合わせて参考になります。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。