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

css 3d 回転ローダー9種 — 値で面切替

css 3d 回転を使うローダーは、キーフレームで揺れるだけの飾りではありません。ローダーの最初のフレームでは平らな面が一枚だけ見えるが、値がしきい値を超えるたびに六面体の面が切り替わり、ワイヤーフレームの頂点が定位置へ集まり、最後のフレームでは内側リングの歳差角が最大に広がった分だけ値が満ちたと読める —

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

下の九つの立体ローダーはすべて同じ二つのCSSプロパティ — 親要素の perspective と子要素の transform-style: preserve-3d — と、毎フレーム同じ一つの数値で動いています。ただし「完了」という状態の表し方はパターンごとに違い、面のインデックス(01)、補間の割合(02)、奥行きの順位(03)、傾きの角度(04)、半径の収縮(05)、塗りのしきい値(06)、反転角度(07)、高さの上昇(08)、歳差円錐(09)と、それぞれ別の幾何学で「値が満ちた」を表現します。並び順は、一つの数値が四つの面のどれかを選ぶだけの単純なケースから、一つの値が同時に二軸の回転を動かさなければならないケースへと進みます。

デモ

上の九つのタイルはそれぞれ小さなDOMの場面です。一つの perspective 親要素の中に数個の絶対配置された要素が入っていて、毎フレームの requestAnimationFrame で呼ばれる render(p) 関数がそれらを3D回転・移動させています。タイルをクリックするか、フォーカスした状態でSpaceキーかEnterキーを押すか、ウィンドウをリサイズすると、そのタイルは現在の進捗値から自分の変形をもう一度計算し直します — ここには単独でループする CSS の @keyframes は一つもなく、ループもイージングも幾何学もすべて一つの数値を読むプレーンなJavaScriptです。

キャンバス物理ローダーとポインター追従ボタンとの違い

九つのキャンバス物理ローダーは、流体の表面や粒子の群れ、セルオートマトンを計算して ctx.fillRect や ctx.arc でピクセルとして描いています — 回転しているDOM要素は一つもなく、毎フレーム塗り直される2Dビットマップがあるだけです。これから紹介する九つはキャンバスのコンテキストに一切触れません。面も頂点もカードもリングもすべて実在するDOM要素で、「3D」らしさは perspective と、スクリプトが計算して style.transform に代入する rotateX・rotateY・translateZ の値だけから生まれています。

描画方法がいちばん近い親戚はポインター追従ボタンの九種で、こちらも実在するDOM要素にCSSの3D変形をかけています。ただし入力の出どころは正反対です。あのボタン九種は mousemove のたびに clientX と clientY を読み、ポインターが今いる方向へ傾きます。つまり形はポインターが今どこにあるかの関数です。これから紹介する九つのローダーはポインター位置を一切読みません。入力はタイマーで進む0から1の進捗値ただ一つなので、04番のリングの収束も09番の内側リングの歳差角の広がりも、カーソルがタイル付近にあろうとなかろうと同じように進みます。

下の累積面積と強度の数値が実際に測っているもの

下の数値はそれぞれのデモを実際に画面収録して測った実測値で、目分量ではありません。累積面積はループ全体でタイルの何%が変化したか、平均強度は変化した箇所のピクセル差分バイト値の平均、動いたフレームは23回のフレーム間差分のうち実際にピクセルが動いた回数を数えています。

# パターン 累積面積 平均強度 動いたフレーム
01 立方体 面しきい値切替 8.577% 92.8 23/23
02 ワイヤーフレーム 頂点補間スナップ 4.867% 87.1 23/23
03 カードスタック 奥行き循環 5.401% 95.2 23/23
04 リング軌道傾き収束 15.808% 102.8 23/23
05 球面点収束 6.127% 150.6 23/23
06 角柱 面塗り回転 16.692% 142.6 23/23
07 フリップパネル 奥行きソート 12.239% 217.6 23/23
08 地形グリッド 遠近隆起 7.95% 141.8 23/23
09 ジャイロスコープ リング歳差 7.29% 88.9 23/23

九つとも23回の測定差分すべてで動いていますが、面積は4.867%から16.692%までかなり幅があります。ローダーはもともと画面上で小さい要素なので、面積が小さいこと自体は欠陥ではありません。この表が本当に示しているのは、css アニメーション 回転 3d のように見える01番の立方体も、07番のパネル反転も、タイルの5分の1にも満たない面積のままほぼ毎フレーム再描画され続けているという点です。

一つの0〜1の値が面・頂点・軌道へ変わる九つの方法

以下のコードブロックはそれぞれ、そのパターン自身のソースファイルから切り出した無編集の断片で、上のデモとは別に書き起こしたものではありません。記事の終わりにあるzip内のファイルを開いても同じ内容です。

01パーセントがしきい値を超えるたびに面が進む六面体ローダー

深度ソートは毎フレーム、保存しておいた法線角と現在の回転角を足したもののコサインで四つの面を並べ直すので、z-indexは常にカメラにいちばん近い面と一致します。パーセント値が FACE_THRESHOLD を超えるたびに立方体は次の90度の面へ進みます。ファイル変換の進捗表示や複数ステップのインストール表示に向いていて、決まった数の段階がすでにあるなら、それがそのまま面の数と対応します。

値がしきい値を超えるたびに次の面へ切り替わる六面体ローダー
新しいタブで開く

01 · 値がしきい値を超えるたびに次の面へ切り替わる六面体ローダー — 計測 累積面積 8.577% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FACE_THRESHOLDNORMALSbackface-visibility
var FACE_THRESHOLD = 25; // 面を切り替えるしきい値(%) -- 変形01の対象
var t0 = 0, raf = 0, paused = false, lastP = 0;

// ペインターのアルゴリズム -- (面の法線角+現在の回転角)のコサインを奥行きとして使い、奥から手前へz-indexを割り当てる
function depthSort(rotY) {
  var withDepth = faces.map(function (el, i) {
    return { el: el, depth: Math.cos((NORMALS[i] + rotY) * Math.PI / 180) };
  });
  withDepth.sort(function (a, b) { return a.depth - b.depth; });
  withDepth.forEach(function (o, idx) { o.el.style.zIndex = String(idx); });
}

function render(p) {
  lastP = p;
  var pct = Math.round(p * 100);
  var idx = Math.min(faces.length - 1, Math.floor(pct / FACE_THRESHOLD));
  var rotY = idx * 90;
  cube.style.transform = 'rotateY(' + rotY + 'deg)';
  depthSort(rotY);
  pctEl.textContent = pct + '%';
}

02進捗の値でワイヤーフレームの頂点が定位置へ集まるローダー

八つの頂点は散らばった固定位置から始まり、Math.pow(p, LERP_EASE_POW) の割合で立方体の角へ補間で近づきます。頂点をつなぐ12本のSVG線も毎フレーム引き直されるので、値が上がるほど輪郭がはっきりしていきます。3Dアセットの読み込みや地図データの取得表示に向いていて、まだ組み上がっている途中の形の方が、先に満タンになったバーより正直に見えます。

進捗値の分だけ頂点が定位置に集まるワイヤーフレームローダー
新しいタブで開く

02 · 進捗値の分だけ頂点が定位置に集まるワイヤーフレームローダー — 計測 累積面積 4.867% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LERP_EASE_POWTARGETStransform-style
function render(p) {
  lastP = p;
  var e = ease(p);
  var rotY = (p * 360 * ROT_TURNS) % 360;
  cube.style.transform = 'rotateX(18deg) rotateY(' + rotY + 'deg)';
  for (var i = 0; i < 8; i++) {
    var sx = SCATTER[i][0], sy = SCATTER[i][1], sz = SCATTER[i][2];
    var tx = TARGETS[i][0], ty = TARGETS[i][1], tz = TARGETS[i][2];
    var cx = sx + (tx - sx) * e;
    var cy = sy + (ty - sy) * e;
    var cz = sz + (tz - sz) * e;
    vtx[i].style.transform = 'translate3d(' + cx + 'px,' + cy + 'px,' + cz + 'px)';
    vtx[i].style.opacity = String(0.25 + 0.75 * e);
  }
  updateLines(e);
  pctEl.textContent = Math.round(p * 100) + '%';
}

03奥のカードが深度順位を計算し直して手前へ進むローダー

rankOf() は各カードの手前からの順位を (i - s) % CARD_COUNT という一つの連続した値として計算するので、スタックの順序は配列を物理的に並べ替えるのではなく、毎フレーム一つの数値から丸ごと再構築されます。複数ステップのチェックアウト進捗やオンボーディング表示に向いていて、一番手前にあるカードが常に「今のステップ」として読めます。

値が上がるたびに奥のカードが奥行き順を変えて手前に来るカードスタックローダー
新しいタブで開く

03 · 値が上がるたびに奥のカードが奥行き順を変えて手前に来るカードスタックローダー — 計測 累積面積 5.401% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ZGAPSHRINKOPACITY_STEP
// カード i の現在の奥行き順位(0が最前面)を進捗の連続的な剰余関数として計算する -- 状態を蓄積せず、
// 毎フレーム一つの p から順序全体を再構築する(ペインターのアルゴリズムと同じ考え方)
function rankOf(i, s) {
  var r = (i - s) % CARD_COUNT;
  if (r < 0) r += CARD_COUNT;
  return r;
}

function render(p) {
  lastP = p;
  var s = p * CARD_COUNT;
  for (var i = 0; i < CARD_COUNT; i++) {
    var rank = rankOf(i, s);
    var z = -rank * ZGAP;
    var y = rank * YOFF;
    var scale = 1 - rank * SHRINK;
    var op = 1 - rank * OPACITY_STEP;
    cards[i].style.transform = 'translateY(' + y + 'px) translateZ(' + z + 'px) scale(' + scale + ')';
    cards[i].style.opacity = String(Math.max(0.25, op));
    cards[i].style.zIndex = String(Math.round((CARD_COUNT - rank) * 1000));
  }
  pctEl.textContent = Math.round(p * 100) + '%';
}

04積み重なったリングが値とともに傾きの差を縮めるローダー

四つのリングはそれぞれ別の傾き角から始まり、進捗に比例して0度へ向けて個別に補間されるので、最初はばらばらの四つのリングに見えていたものが、進捗が満ちた瞬間にぴったり一枚のリングへ収束します。同期進捗の表示や複数レイヤーの読み込み表示に向いていて、並行して走っている複数の処理が一つの完了状態へ合流したように見せられます。

値が上がるほどリング間の傾き差が狭まる軌道ローダー
新しいタブで開く

04 · 値が上がるほどリング間の傾き差が狭まる軌道ローダー — 計測 累積面積 15.808% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

INITIAL_TILTSPIN_TURNSperspective
var INITIAL_TILT = [72, 54, 36, 18]; // 各リングは個別の傾き(度)から始まり -- 値が上がるほど0度へ収束する
var SPIN_TURNS = 0.6;                 // リングの束が1ループで何回転するか

var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var LOOP = 2;
var REDUCE_P = 0.7;
var t0 = 0, raf = 0, paused = false, lastP = 0;

function render(p) {
  lastP = p;
  var spinY = (p * 360 * SPIN_TURNS) % 360;
  for (var i = 0; i < rings.length; i++) {
    var tilt = INITIAL_TILT[i] * (1 - p); // 進捗に比例して正面向き角度(0度)へ補間する
    rings[i].style.transform = 'rotateY(' + spinY + 'deg) rotateX(' + tilt + 'deg)';
  }
  pctEl.textContent = Math.round(p * 100) + '%';
}

05球面上の点が一点へ収束するローダー

固定されたフィボナッチ球分布 — Math.random ではなく黄金角の定数 — が各点の基準位置を決め、すべての点が原点へ向けて (1 - p) の比率で縮んでいくので、進捗が満ちると散らばっていた点群が一点にまとまります。データ同期完了のバッジやアプリ初期化画面に向いていて、散らばった場が一点に落ち着く様子が「すべて揃った」ことをよく表します。

球面上の点が一点に集まる収束ローダー
新しいタブで開く

05 · 球面上の点が一点に集まる収束ローダー — 計測 累積面積 6.127% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GOLDEN_ANGLESPHERE_Rtransform-style
function render(p) {
  lastP = p;
  var rotY = (p * 360 * ROT_TURNS) % 360;
  var shrink = 1 - p; // 中心(0,0,0)へ向かう収束比
  sphere.style.transform = 'rotateY(' + rotY + 'deg) rotateX(14deg)';
  for (var i = 0; i < POINT_COUNT; i++) {
    var b = BASE[i];
    var x = b[0] * shrink, y = b[1] * shrink, z = b[2] * shrink;
    pts[i].style.transform = 'translate3d(' + x + 'px,' + y + 'px,' + z + 'px)';
  }
  pctEl.textContent = Math.round(p * 100) + '%';
}

06回転する多角柱が通過した順に面を塗りつぶすローダー

fillAmount() は角柱の六つの面それぞれに独自のしきい値(i / PRISM_SIDES)と短いフェードイン区間を与えているので、css スピナー という言葉で検索して出てくるものとはまったく違う挙動になります — 形全体の色が一斉に変わるのではなく、回転する角柱の上に塗りの境界線がそのまま見え続けます。アンケートの進捗ステップや在庫充填の表示に向いていて、残り時間よりもどれだけ埋まったかの方が意味を持つ場面に合います。

回転しながら通過した面から色が満ちる多角柱ローダー
新しいタブで開く

06 · 回転しながら通過した面から色が満ちる多角柱ローダー — 計測 累積面積 16.692% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PRISM_SIDESFILL_SPANtransform-style
function fillAmount(i, p) {
  var threshold = i / PRISM_SIDES;
  var raw = (p - threshold) / FILL_SPAN;
  if (raw < 0) return 0;
  if (raw > 1) return 1;
  return raw;
}

function render(p) {
  lastP = p;
  var rotY = (p * 360 * ROT_TURNS) % 360;
  prism.style.transform = 'rotateX(-14deg) rotateY(' + rotY + 'deg)';
  for (var i = 0; i < PRISM_SIDES; i++) {
    fills[i].style.opacity = String(fillAmount(i, p));
  }
  pctEl.textContent = Math.round(p * 100) + '%';
}

07パネルが反転するたびに前後の順序を計算し直すローダー

currentIndex() と depthRank() はどちらも同じ p から導かれるので、0度から180度の間で反転中の一枚のパネルが常にカメラにいちばん近く配置され、隣のパネルはそのインデックスからの距離に応じて奥へ退きます。チュートリアルのステップ遷移やカード形式のウォークスルー進捗に向いていて、各ステップが目に見えて一枚ずつ反転していきます。

パネルが反転するたびに前後順を再計算するフリップローダー
新しいタブで開く

07 · パネルが反転するたびに前後順を再計算するフリップローダー — 計測 累積面積 12.239% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RANK_ZGAPSTEPbackface-visibility
// パネル i の回転角 -- 自分の区間の中だけ0〜180度で線形に変化し、区間前は0、区間後は180で固定する
function flipAngle(i, p) {
  var start = i * STEP, end = start + STEP;
  if (p < start) return 0;
  if (p >= end) return 180;
  var local = (p - start) / STEP;
  return local * 180;
}
// パネル i の奥行き順位 -- 現在反転中のパネルとの距離。遠いほど奥へ押しやられる
function depthRank(i, p) {
  var cur = currentIndex(p);
  return Math.abs(cur - i);
}

function render(p) {
  lastP = p;
  for (var i = 0; i < PANEL_COUNT; i++) {
    var angle = flipAngle(i, p);
    var rank = depthRank(i, p);
    panels[i].style.transform = 'translateZ(' + (-rank * RANK_ZGAP) + 'px) translateY(' + (rank * RANK_YOFF) + 'px) rotateY(' + angle + 'deg)';
    panels[i].style.zIndex = String(PANEL_COUNT - rank);
  }
  pctEl.textContent = Math.round(p * 100) + '%';
}

08グリッドの点が値に応じて遠近感のある地形として隆起するローダー

各グリッド点の高さは0から始まり、rise = weight * p * HEIGHT_SCALE の式で上がっていきます。固定の rotateX() の傾きが平らなグリッドに斜めの視点を与えているので、隆起していく点群は棒グラフというより地形のように見えます。地図データの読み込みやダッシュボードの初期読み込み背景に向いていて、読み込み中の状態がそのままこれから表示される内容のプレビューにもなります。

格子点が値の分だけ盛り上がり地形に見える遠近ローダー
新しいタブで開く

08 · 格子点が値の分だけ盛り上がり地形に見える遠近ローダー — 計測 累積面積 7.95% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GRID_COLSHEIGHT_SCALETILT
function render(p) {
  lastP = p;
  var rotY = (p * 360 * ROT_TURNS) % 360;
  grid.style.transform = 'rotateX(' + TILT + 'deg) rotateZ(' + rotY + 'deg)';
  for (var i = 0; i < pts.length; i++) {
    var b = base[i];
    var rise = b[2] * p * HEIGHT_SCALE;
    pts[i].style.transform = 'translate3d(' + b[0] + 'px,' + (-rise) + 'px,' + b[1] + 'px)';
  }
  pctEl.textContent = Math.round(p * 100) + '%';
}

09内側のリングが値とともに歳差角を広げるローダー

内側リングの回転軸は、進捗とともに半径(PRECESSION_FACTOR * p)が広がる円錐を描きます。円錐の位相のコサインとサインでX軸とZ軸に振り分けているので、軸そのものが円を描くように動きます。長時間の読み込み画面やバックグラウンド同期の表示に向いていて、最初の数秒を過ぎてもまだ生きている感じを保ちたい場面によく合います。

値が上がるほど内側リングの歳差角が広がるジャイロスコープローダー
新しいタブで開く

09 · 値が上がるほど内側リングの歳差角が広がるジャイロスコープローダー — 計測 累積面積 7.29% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PRECESSION_FACTORPRECESSION_TURNSOUTER_TILT
// 内側リングの回転軸が描く円錐 -- 歳差角(進捗とともに増える)をコーン位相のcos/sinでX軸・Z軸成分に分け、
// 軸自体が円を描くことで歳差運動を擬似的に再現する
function render(p) {
  lastP = p;
  var spinY = (p * 360 * SPIN_TURNS) % 360;
  var maxAngle = PRECESSION_FACTOR * p;
  var conePhase = (p * 360 * PRECESSION_TURNS) % 360;
  var rad = conePhase * Math.PI / 180;
  var tiltX = OUTER_TILT + maxAngle * Math.cos(rad);
  var tiltZ = maxAngle * Math.sin(rad);
  outer.style.transform = 'rotateY(' + spinY + 'deg) rotateX(' + OUTER_TILT + 'deg)';
  inner.style.transform = 'rotateY(' + spinY + 'deg) rotateX(' + tiltX + 'deg) rotateZ(' + tiltZ + 'deg)';
  pctEl.textContent = Math.round(p * 100) + '%';
}

FACE_THRESHOLDが時間内に終わらない落とし穴になって壊れる理由

01番の落とし穴は Math.min(faces.length - 1, Math.floor(pct / FACE_THRESHOLD)) の中にあるクランプです。面は四つしかないので、最後のインデックス(3)は pct が 3 * FACE_THRESHOLD を超えた瞬間に到達します。既定値の25ならpctが75の時点で、ループの残り25ポイントの間は最後の面が静止します。「もっと反応をよくしよう」と FACE_THRESHOLD を下げると、このクランプはむしろ早く効いてしまいます。18まで下げると最後の面はpct 54で固定され、静止する幅は25ポイントから46ポイントへ広がります。しきい値を下げると最初の三面の切り替えは速くなりますが、最後の面はループの半分近く止まって見えるようになり、狙いとは正反対の結果になります。直すべきなのは FACE_THRESHOLD をさらに下げることではなく、面の数に合わせて FACE_THRESHOLD = Math.ceil(100 / faces.length) とし、四面すべてに均等な取り分を与えることです。

最初に触ってみる価値がある三つの値と、reduced-motion が取り除くもの

変形 変更する値 どう感じるはずか
立方体の面切替しきい値を調整する 01 FACE_THRESHOLD 25 → 18 しきい値を下げると、より小さい進捗値で次の面へ切り替わるはずなので、面の切り替えがより頻繁に起きて感じられます。
ワイヤーフレームの頂点補間速度を調整する 02 LERP_EASE_POW 1.8 → 1.2 頂点補間の指数を下げると、頂点がより早く目標座標へ到達するはずなので、進捗の早い段階から輪郭がはっきりして感じられます。
ジャイロスコープの歳差角係数を調整する 09 PRECESSION_FACTOR 26 → 34 係数を上げると、同じ進捗値でも内側リングの歳差角がより大きく広がるはずなので、揺れがより強く感じられます。

九つとも prefers-reduced-motion はページが最初に読み込まれた瞬間に一度だけ読み、毎フレーム読むことはありません。このメディアクエリが reduce を返した場合、ループは一度も始まらず、代わりに固定の進捗値で一枚の静止フレームだけが描かれます。すべてのタイルには role="application"、tabindex="0"、aria-label も付いていて、クリック・Enterキー・Spaceキー(どちらもループの再生・停止を切り替える)・Rキー(再スタート)を同じハンドラーが受け取り、さらにリサイズ時にはそのサイズでタイルを描き直すリスナーもあります。マウスを使わないアクセシビリティの利用者でも、九つのどれでも再生・一時停止・再スタートができます。パターンごとに一つずつある九つのソースファイルは、この記事のデモと同じフォルダの中で gfc9k6p7 の先に置かれています。上の落とし穴に向けて FACE_THRESHOLD を下げる前に一度開いてみてください。回り込まない面は測定表の上では問題なく見えても、実際の読み込み画面では止まって見えることがあります。

この九つを実際にどこで使うか

01番の面切替立方体は、ファイル変換の進捗画面や複数ステップのインストールウィザードに向いています。決まった数の段階がすでにあるなら、それがそのまま面の数と対応しますし、深度ソートのおかげで段階ごとに余分なDOMノードを増やさずに見えている面を正しく保てます。

03番のカードスタック循環と07番のフリップパネルは、どちらもチェックアウトやオンボーディングの流れに向いています。人が見つめている面がもともとカード状だからです。違いは各ステップがどれだけ「確定した」感じにしたいかで、03番はカードを扇状のスタックのまま保ち続けるのに対し、07番は一枚のパネルを完全に手前へ寄せて隣のパネルを0度か180度のどちらかに固定します。

09番のジャイロスコープ歳差は、バックグラウンド同期の表示や長時間の読み込み画面で選ぶ価値があります。ループが続く限り揺れが広がり続けるので、94%で静かに止まってしまうバーよりも「まだ動いている」ことをずっとよく伝えられるからです。

css ローディング アニメーション の実例を他でも探すなら

css スピナー で検索すればすぐに見つかりますし、それも出発点としては悪くありません。ただし出てくるものの大半は一つの要素が CSS の @keyframes 一本でループしているだけです。深度ソートも補間カーブも歳差円錐もその下にはありません。@keyframes は一つのプロパティを二つの固定値の間で補間するだけだからです。06番の回転する角柱も09番のジャイロスコープも、プロパティが値から値へイージングするのではなく、毎フレームプレーンなJavaScriptの数値が変形全体を計算し直しているからこそあの動きになります。すぐプロジェクトに組み込める動く css 3d 回転 のパターンを探しているなら、上の九つがまさにそれです。各パターンの下にあるソースを開いてファイルをコピーし、自分のアップロードやリクエストが実際に返す進捗の値に差し替えてください。

3Dローダーを組む前に答えておきたい、よくある質問

07番を一枚だけのカードで使う3Dカードフリップとしても使えますか?

はい。PANEL_COUNT を1にしても flipAngle() は進捗全体を通じて0度から180度へ線形に補間し続けるので、存在しなくなった隣のパネルとの深度順位計算を気にせず、一枚だけ反転するカードとして使えます。

01番の立方体と06番の角柱はどちらもしきい値で塗るのに、なぜ見え方が違うのですか?

01番はしきい値を超えた瞬間に見えている面全体を一気に切り替えますが、06番はそれぞれの面がしきい値を超えてから FILL_SPAN の間だけ徐々にフェードインします。片方は瞬間的な切り替え、もう片方は短いクロスフェードで、どちらも同じ進捗値に対するしきい値の比較です。

この九つにライブラリやWebGL、CDNスクリプトは必要ですか?

いいえ。九つとも perspective、transform-style: preserve-3d、そして requestAnimationFrame ループの中で style.transform を動かすプレーンなDOM要素だけで動いています。この記事の裏にある九つのファイルのどこにも Three.js もキャンバスのWebGLコンテキストも外部scriptタグもありません。

出典: MDN — perspective · MDN — transform-style · MDN — rotate3d() · MDN — prefers-reduced-motion · MDN — progressbar role

関連記事: ローディング アニメーション js 9パターン — 物理演算で作る · 【一行でコピペOK】CSS 3D アニメーション9選 · マウス 追従 アニメーション css で作る3D場面9選 — 一つ目は同じ九種構成をDOM変形の代わりに2Dキャンバスで動かしたもの、二つ目はCSS 3Dの近い親戚で別の回転値の組み合わせで立方体カルーセルを作ったもの、三つ目は上の04番や09番が時間だけから動くのとは逆に、九つの場面をポインター位置から動かしています。

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

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