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

css corner-shape 9選 — border-radiusで切らない角の処理

9つのコーナー形状のうち、1つはスーパー楕円指数の値が円と四角の間を流れ、1つはsquircle比率の値でカードの角が補間され、1つはベベル角度の値が増すほど面の幅が広がります。この9つは、css corner-shape の対応を起動時に確かめながら角の曲線そのものを動かす、コピーしてすぐ試せる実演です。

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

css corner-shape は、border-radius と組み合わせて角の曲線そのものを指定する実験的なプロパティで、下の9項目はすべて起動時に CSS.supports で対応を確かめてから値を書き込み、対応していない環境では border-radius の近似に切り替えます。グリッドを見ていると、カードごとに1つの数値 — 指数・角度・比率・インセット — が動き、それに合わせて角の曲線が変わるのがわかります。ランダムな明滅はどこにもありません。

9つのパターンの解剖 — どの値が角の曲線を変えるか

次の表には、項目ごとに3つの実測値が入っています。動く角の処理がフレームのうちどれだけの面積を累積で塗り替えたか、1ピクセルあたりの平均強度がどれだけだったか、そして23フレーム中いくつのフレームで実際にピクセルが動いたか、この3つです。

項目 累積面積 強度平均 動いたフレーム
01 1.36% 109.8 23/23
02 0.33% 73.2 23/23
03 1.99% 90.0 21/23
04 3.21% 94.7 8/23
05 1.01% 109.7 23/23
06 3.25% 107.9 13/23
07 0.80% 107.2 15/23
08 1.42% 82.0 23/23
09 0.43% 82.8 23/23

累積面積が最も広いのは06だけで、最も狭いのは02だけです。強度が最も高いのは01の109.8で、05の109.7とは0.1しか離れておらず、順位は実測値の数字だけが区別します。強度が最も低いのも02、動いたフレームが最も少ないのは04の23フレーム中8で、ノッチはタブの切り替えが実際に進んでいるあいだだけ動き、切り替えと切り替えのあいだは完全に静止しているためです。表の数値はどれもこの9項目をこのまま録画した実測値であり、再生環境が変われば値も変わります。

値を動かす計算方式で9項目を分けると、ちょうど4つのグループに分かれます。それぞれの計算層は独立したファイルにあります。

01・02 — 経過時間の比率を関数に通してから形状にするもの

01は SWEEP_MS で割った経過時間を三角波にし、それを5次のスムーザーステップで曲げてから MIN_N から SUPER_N までの指数に映射します。02は角度の値を Math.tan に通して、固定の BEVEL_RY から横軸半径を逆算し、Math.hypot で斜面幅を測ります。どちらも、CSSトランジションに中間値を任せるのではなく、素直な経過時間の比率を明示的な関数で角の形状に変えているグループです。

03・04・09 — 目標を通り過ぎたり、イージングで目標に近づく値

03は押している時間の比率を3次ease-out(上昇時)と3次ease-in(下降時)で曲げてスクープ半径に映射します。04のノッチ深さは springOvershoot 関数に従い、目標を一瞬通り過ぎてから戻って落ち着きます。09は離したあと、固定時間のトゥイーンではなく 1 - Math.exp(-INSET_SPRING_K * dt) の指数減衰でゆっくり目標へ戻ります。

05・07・08 — 毎フレーム計算される波とサイクル

05は角ごとに位相を Math.PI / 2 ずつずらしたサイン値4つを同時に計算するため、4つの半径が同じタイミングで膨らむことはありません。07は固定の5段リスト RUNGS を、段のあいだを3次ease-in-outで補間しながら上り下りします。08は半径を (1 - Math.cos(t * Math.PI * 2)) / 2 で往復させます — 位相を合わせたコサインなので、値は中間値ではなく下限から始まります。

06 — 慣性lerpで追いかける補間比率

06は比率を目標へ瞬間移動させません。毎フレーム、残りの距離のうち 1 - Math.exp(-LERP_K * dt) の割合だけを詰めていくため、同じホバー操作が60fpsでも144fpsでも同じ速さで読める性質です。

パターン別コード

01スクワークルカード — スーパー楕円指数の値が円と四角の間を流れ、カードの角の稜線が変わる方式

指数の値は5次スムーザーステップで曲げた三角波に乗って MIN_N(1)から SUPER_N(4.0)までを SWEEP_MS ごとに往復し、それぞれの値が固定40px半径の cornerShape = superellipse(n) として書き込まれるため、同じ1枚のカードが円に近い形から真のスクワークルを経て四角へと流れます。アプリアイコンのプレビューカードやギャラリーのサムネイルカードに合う方式です。クリックすると指数が3つのプリセット値のあいだを自分で止まって回ります。

数値が語ること

項目01は23フレーム中23枚で動きながら累積面積1.36%を占め、強度平均は9項目中で最も高い109.8に達しますが、これはカードの縁全体がステップごとに再び丸まるためで、僅差の項目05(109.7)より0.1ポイントだけ高い値です。

5次スムーザーステップが三角波の折り返しを丸めるため、指数がSUPER_N(4.0)に達する瞬間も動きが不連続に感じられません。

スクワークルカード — スーパー楕円指数の値が円と四角の間を流れ、カードの角の稜線が変わる方式
新しいタブで開く

01 · スクワークルカード — スーパー楕円指数の値が円と四角の間を流れ、カードの角の稜線が変わる方式 — 計測 累積面積 1.36% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SUPER_NSWEEP_MS$subject-blue
  // 計算層: スムーザーステップ(5次多項式, Perlin)で往復三角波を滑らかな流れに広げる — 線形トゥイーンではない
  function smootherstep(u) { return u * u * u * (u * (u * 6 - 15) + 10); }
  function triangle(x) { var f = x - Math.floor(x); return f < 0.5 ? f * 2 : (1 - f) * 2; }

  function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }

  function setN(n) {
    if (SUPPORTS) {
      card.style.cornerShape = 'superellipse(' + n.toFixed(3) + ')';
      card.style.borderRadius = '40px';
    } else {
      // 近似フォールバック: スーパー楕円指数が大きいほど半径を狭め、角ばった印象を模倣する(正確な曲線ではない)
      card.style.borderRadius = (40 / n).toFixed(2) + 'px';
    }
    readout.textContent = '指数値 ' + n.toFixed(2);
  }

02ベベルチップ — ベベル角度の値が増すほどチップの角の面の幅の値が共に大きくなる方式

角度の値が MIN_DEG(8)から BEVEL_DEG(18)へ上がるあいだ、横軸半径は BEVEL_RY × Math.tan(rad) で計算され、斜面幅は Math.hypot(rx, BEVEL_RY) で測られるため、切断面は直線ではなくタンジェントに沿って細い線から本物の面へ育ちます。値札・ラベルチップやステータスバッジに合う方式です。

数値が語ること

項目02は23フレーム中23枚で動きますが累積面積は0.326%と9項目中で最小で、強度平均も73.2と最も低く、ベベル面の変化が視覚的に小さいことを示しています。

斜面幅はBEVEL_RYとMath.tan(rad)から導かれるため、角度が上がるほど面の幅も加速的に広がります。

ベベルチップ — ベベル角度の値が増すほどチップの角の面の幅の値が共に大きくなる方式
新しいタブで開く

02 · ベベルチップ — ベベル角度の値が増すほどチップの角の面の幅の値が共に大きくなる方式 — 計測 累積面積 0.326% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BEVEL_DEGBEVEL_RY$subject-blue-deep
  // 計算層: ベベル角度の値から横軸半径を三角比で逆算し、2つの半径の斜辺の長さを斜面幅とする
  function applyAngle(deg) {
    var rad = deg * Math.PI / 180;
    var rx = BEVEL_RY * Math.tan(rad);
    var facetWidth = Math.hypot(rx, BEVEL_RY);
    if (SUPPORTS) {
      chip.style.borderTopRightRadius = rx.toFixed(2) + 'px ' + BEVEL_RY + 'px';
    } else {
      // 近似フォールバック: 対角の斜面の代わりに同じ半径サイズの丸い角で模倣する
      chip.style.borderTopRightRadius = facetWidth.toFixed(2) + 'px';
    }
    readout.textContent = '斜面幅 ' + facetWidth.toFixed(1);
  }

03スクープボタン — 角が内側へえぐれるscoop曲線の値が動き、ボタンの輪郭が変わる方式

スクープ半径は押しているあいだ SCOOP_MIN(4)から SCOOP_MAX(28)へ3次ease-outで伸び、離すと3次ease-inで戻るため、凹んだ切り取りは最初に速く深くなってからゆっくり緩み、ずっと一定の速さで動くわけではありません。次のステージへ誘うボタンやタグ削除ボタンに合う方式です。キーボードでも押し続けが再現できます。

数値が語ること

項目03は23フレーム中21枚で動き、累積面積1.99%、強度平均90.0で、押している間だけ伸びるscoop半径の性質上、動くフレーム数が9項目の中間に位置します。

3次ease-outとease-inを押す/離すで使い分けているため、伸びと戻りの速さの印象がわずかに異なります。

スクープボタン — 角が内側へえぐれるscoop曲線の値が動き、ボタンの輪郭が変わる方式
新しいタブで開く

03 · スクープボタン — 角が内側へえぐれるscoop曲線の値が動き、ボタンの輪郭が変わる方式 — 計測 累積面積 1.99% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCOOP_MAXHOLD_MS$subject-mint
  // 計算層: 押している時間の比率を3次ease-outで曲げてスクープ半径にマッピングする — 線形の増加ではない
  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
  function easeInCubic(t) { return t * t * t; }

  function applyScoop(r) {
    if (SUPPORTS) {
      btn.style.cornerShape = 'scoop';
      btn.style.borderRadius = r.toFixed(2) + 'px';
    } else {
      // 近似フォールバック: 凹んだスクープの代わりに同じ半径の丸い角で模倣する(凹と凸の向きだけが異なる)
      btn.style.borderRadius = r.toFixed(2) + 'px';
    }
    readout.textContent = 'スクープ半径 ' + r.toFixed(1);
  }

04ノッチタブ — 選択されたタブの角にノッチ深さの値の分だけ溝が掘られる方式

タブが切り替わるたび、ノッチ深さは springOvershoot に従うため、溝は一瞬 NOTCH_MAX(14)を通り過ぎてから沈んで落ち着き、開いているタブは輪郭だけで読み取れます。ドキュメントエディタのタブバーやブラウザタブUIに合う方式です。

数値が語ること

項目04は23フレーム中わずか8枚だけが動き、これは9項目の中で最少の値ですが、累積面積は3.212%とむしろ大きく、タブが切り替わる瞬間にノッチ深さが大きく動いてすぐ落ち着くスプリングオーバーシュートの性質を表しています。

springOvershootの定数c4が振動の周期を決めるため、ノッチはNOTCH_MAX(14)を一瞬超えてから収まります。

ノッチタブ — 選択されたタブの角にノッチ深さの値の分だけ溝が掘られる方式
新しいタブで開く

04 · ノッチタブ — 選択されたタブの角にノッチ深さの値の分だけ溝が掘られる方式 — 計測 累積面積 3.212% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

NOTCH_MAXSPRING_MS$subject-sky
  // 計算層: タブが変わるときノッチ深さが目標値をわずかに通り過ぎてから沈む減衰振動(スプリングオーバーシュート)で動く
  function springOvershoot(t) {
    if (t <= 0) return 0;
    if (t >= 1) return 1;
    var c4 = (2 * Math.PI) / 3;
    return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
  }

  function applyDepth(tab, depth) {
    if (SUPPORTS) {
      tab.style.cornerShape = 'notch';
      tab.style.borderRadius = Math.max(0, depth).toFixed(2) + 'px';
    } else {
      // 近似フォールバック: 掘られたノッチの代わりに同じ大きさの丸い角で模倣する(溝と丸みだけが異なる)
      tab.style.borderRadius = Math.max(0, depth).toFixed(2) + 'px';
    }
  }

05ウェーブパネル — 4つの角の曲線の値が位相差を持って順に揺れる方式

各角は自分自身の半径を BASE_R + AMP × Math.sin(t × 2π + i × π/2) で計算するため、4つの値はそれぞれ四半周期ずつずれており、膨らみが WAVE_MS ごとにパネルを一周する波のように見えます。ヒーローセクションの背景パネルやイントロカードに合う方式です。クリックで波を一時停止し、もう一度クリックで再開できます。

数値が語ること

項目05は23フレーム中23枚で動き、累積面積1.01%、強度平均109.7と項目01に次ぐ高さで、4つの角が位相をずらして順に膨らむため強度の高さが持続します。

4つの角の位相差はちょうど90度(π/2)なので、膨らみは対角ではなく隣り合う角から順に移ります。

ウェーブパネル — 4つの角の曲線の値が位相差を持って順に揺れる方式
新しいタブで開く

05 · ウェーブパネル — 4つの角の曲線の値が位相差を持って順に揺れる方式 — 計測 累積面積 1.01% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AMPWAVE_MS$subject-lilac
  // 計算層: 角ごとに位相をPI/2ずつずらした4つのサイン波を同時に計算し、波のように交互に膨らませる
  function corners(t) {
    var out = [];
    for (var i = 0; i < 4; i++) {
      var phase = i * (Math.PI / 2);
      var r = BASE_R + AMP * Math.sin(t * Math.PI * 2 + phase);
      out.push(Math.max(2, r));
    }
    return out;
  }

  function applyCorners(rs) {
    for (var i = 0; i < 4; i++) {
      if (SUPPORTS) panel.style.cornerShape = 'superellipse(2.2)';
      panel.style['border' + CORNERS[i] + 'Radius'] = rs[i].toFixed(2) + 'px';
    }
    readout.textContent = '位相ずれ ' + (rs[0] - rs[2]).toFixed(1);
  }

06ホバーモーフィング — 待機状態とホバー状態の角の曲線の値が補間されて移る方式

ホバーが変わるたび、補間比率は0か1へ指数減衰でゆっくり追いつき、半径はその比率に乗って ROUND_R(48)と BEVEL_R(6)のあいだを移り、シェイプキーワード自体は中間点で round から bevel へ渡ります。ナビゲーション項目やフィルターボタンに合う方式です。キーボードのフォーカスでも同じ遷移が走ります。

数値が語ること

項目06は累積面積3.251%と9項目中で最大ですが、23フレーム中動くのは13枚にとどまり、指数減衰で比率がゆっくり目標へ追いつく慣性lerpの性質上、動く範囲は広いものの動くフレーム数は中間にとどまります。

LERP_Kの値が大きいほど比率の追いつきが速まり、round/bevelというシェイプキーワード自体も比率0.5で切り替わります。

ホバーモーフィング — 待機状態とホバー状態の角の曲線の値が補間されて移る方式
新しいタブで開く

06 · ホバーモーフィング — 待機状態とホバー状態の角の曲線の値が補間されて移る方式 — 計測 累積面積 3.251% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LERP_KROUND_R$subject-blue-deep
  // 計算層: 比率の値を目標へ瞬間移動させず、指数減衰(dtベースの慣性lerp)で追いつかせる — 2つの形の間の補間比率である
  var last = 0;
  function stepRatio(ts) {
    var dt = last ? (ts - last) / 1000 : 0;
    last = ts;
    var follow = 1 - Math.exp(-LERP_K * dt);
    ratio += (target - ratio) * follow;
    if (Math.abs(ratio - target) < 0.001) ratio = target;
  }

  function applyMorph() {
    var r = ROUND_R + (BEVEL_R - ROUND_R) * ratio;
    var shape = ratio < 0.5 ? (SUPPORTS_ROUND ? 'round' : null) : (SUPPORTS_BEVEL ? 'bevel' : null);
    if (shape) card.style.cornerShape = shape;
    card.style.borderRadius = r.toFixed(2) + 'px';
    readout.textContent = '補間比率 ' + Math.round(ratio * 100) + '%';
  }

07半径のはしご — 1枚のカードの半径値を固定の5段はしごで上り下りし、同じ曲線の質感を見る方式

カードは RUNGS リスト[4, 12, 20, 28, 36]を上り下りし、goTo は現在の補間中の半径を新しい出発点として記録するため、段が変わっても値が途中で飛びません。デザインシステムの角トークン規格ページに合う方式です。矢印キーでも段を送れます。

数値が語ること

項目07は23フレーム中15枚で動き、累積面積0.797%、強度平均107.2で、5段のはしごを3次ease-in-outで補間するため段の切り替わり付近でだけ値が大きく動きます。

goToが呼ばれるたびに現在値をfromRとして記録するため、はしごの途中で方向が変わっても値は飛びません。

半径のはしご — 1枚のカードの半径値を固定の5段はしごで上り下りし、同じ曲線の質感を見る方式
新しいタブで開く

07 · 半径のはしご — 1枚のカードの半径値を固定の5段はしごで上り下りし、同じ曲線の質感を見る方式 — 計測 累積面積 0.797% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RUNGSSTEP_MS$subject-pink
  // 計算層: はしごの段の間を3次ease-in-outで補間する — 段が変わっても値は途切れない
  function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }

  function goTo(nextIdx, ts) {
    nextIdx = Math.max(0, Math.min(RUNGS.length - 1, nextIdx));
    if (nextIdx === idx) return;
    fromR = currentR(ts);
    idx = nextIdx;
    toR = RUNGS[idx];
    stepStart = ts;
    rungEl.textContent = String(idx + 1);
  }

  function currentR(ts) {
    var t = STEP_MS > 0 ? Math.min(1, (ts - stepStart) / STEP_MS) : 1;
    return fromR + (toR - fromR) * easeInOutCubic(t);
  }

08フォールバック並び比較 — 同じ半径値を与えた対応ペインとフォールバックペインの輪郭を並べて比べる方式

2つのペインはどちらも R_MIN(6)と R_MAX(34)のあいだをコサインで往復する同じ半径を受け取りますが、左のペインは対応環境でのみ cornerShape = superellipse(2.6) を加え、右のペインは常に border-radius の近似のままなので、2つのレンダリングがどこで分かれるかを目で確かめられます。ブラウザ対応の案内画面や互換性表に合う方式です。左のキャプションは、開いた環境が対応していないとき自分で書き換わります。

数値が語ること

項目08は23フレーム中23枚で動き、累積面積1.415%、強度平均82.0で、対応ペインと近似ペインの両方が同じコサイン往復の半径を受け取るため、2つの輪郭が同時に動きます。

SUPER_N(2.6)は対応ペインにだけ適用され、右のペインはborder-radiusの近似だけを受け取り続けます。

フォールバック並び比較 — 同じ半径値を与えた対応ペインとフォールバックペインの輪郭を並べて比べる方式
新しいタブで開く

08 · フォールバック並び比較 — 同じ半径値を与えた対応ペインとフォールバックペインの輪郭を並べて比べる方式 — 計測 累積面積 1.415% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

R_MAXSWEEP_MS$subject-lilac
  // 計算層: 半径をコサインで往復させる — サインではなく位相を合わせたコサインで、0秒に下限から始まる
  function radiusAt(t) {
    var c = (1 - Math.cos(t * Math.PI * 2)) / 2;
    return R_MIN + (R_MAX - R_MIN) * c;
  }

  function applyBoth(r) {
    if (SUPPORTS) {
      nativeBox.style.cornerShape = 'superellipse(' + SUPER_N.toFixed(1) + ')';
      nativeBox.style.borderRadius = r.toFixed(2) + 'px';
    } else {
      nativeBox.style.borderRadius = r.toFixed(2) + 'px';
    }
    // 右側のペインは常にborder-radiusの近似だけを使う — 2つのレンダリングの違いを並べて比べる
    shimBox.style.borderRadius = r.toFixed(2) + 'px';
    readout.textContent = '現在の半径 ' + r.toFixed(1);
  }

09内側フレーム — 外側曲線と内側曲線の値が別々に置かれ、二重の縁が生まれる方式

外側のフレームは固定の OUTER_R(40)半径を保ち、内側の曲線は INSET_MIN(6)と INSET_MAX(22)のあいだを呼吸するインセット値から Math.max(2, OUTER_R − inset) で計算されるため、2つの輪郭の間隔は曲線に沿って変わり、流れる二重の縁ができます。プロフィールカードの縁や認証マークのフレームに合う方式です。押しっぱなしにすると内側の曲線が沈み込みます。

数値が語ること

項目09は23フレーム中23枚で動き、累積面積0.428%、強度平均82.8で、外側フレームは固定のまま内側の曲線だけがインセット値に応じて動くため、変化の面積は9項目の中でも小さめです。

OUTER_R(40)は固定のまま、INSET_MIN(6)からINSET_MAX(22)のあいだでinsetだけが呼吸するように動きます。

内側フレーム — 外側曲線と内側曲線の値が別々に置かれ、二重の縁が生まれる方式
新しいタブで開く

09 · 内側フレーム — 外側曲線と内側曲線の値が別々に置かれ、二重の縁が生まれる方式 — 計測 累積面積 0.428% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

OUTER_RINSET_MAX$subject-mint
  // 計算層: インセット算術 — 内側半径 = 外側半径 − インセット。インセットが大きいほど内側の曲線が深く削られる
  function innerRadius(inset) { return Math.max(2, OUTER_R - inset); }

  function applyInset(inset) {
    var r = innerRadius(inset);
    if (SUPPORTS) {
      frame.style.cornerShape = 'superellipse(3)';
      inner.style.cornerShape = 'superellipse(3)';
    }
    frame.style.borderRadius = OUTER_R + 'px';
    inner.style.borderRadius = r.toFixed(2) + 'px';
    readout.textContent = 'インセット ' + inset.toFixed(1);
  }

値が稜線と面を変える基準 — 指数値・角度値・比率値

9つの項目はすべて起動時にCSS.supportsでcorner-shapeの対応を確認し、対応する環境だけがsuperellipse・scoop・notch・bevelといった実際の形状キーワードを受け取ります。対応しない環境では、どの項目もJavaScriptで計算したborder-radiusの値に静かに切り替わり、輪郭の丸さや角度の近似だけで同じ操作感を保ちます。08はこの2つの描画を1つの画面で並べる項目で、左のペインだけが対応環境でsuperellipse(2.6)を追加で受け取り、右のペインは常にborder-radiusの近似のままなので、対応・非対応の違いを実際に見比べられます。ここで言う近似は、同じ半径の値を丸い角として当てはめるという意味での近似であり、角の曲線そのものを描き直すポリフィルとは役割が違います。この呼び分けの意味は、後のFAQでもう一度説明に立ち上がります。

9項目はいずれも起動直後に window.matchMedia('(prefers-reduced-motion: reduce)') の値を reduce という変数に保存しており、CSSの transition を止めるだけでなく、JavaScript側のアニメーション分岐そのものにもこの値を使います。たとえば04は reduce が真のとき springOvershoot の計算を経由せず、ノッチ深さをいきなり目標値まで進めます。09は reduce が真だと内側の曲線のインセットが呼吸する代わりに1つの固定値へ止まります。動きの有無は項目ごとに実装が分かれますが、matchMediaで検知するという入口は9項目で共通しています。固定の仕方も項目ごとに読み方が違い、01は流れの到着点である SUPER_N に、07ははしごの最上段に、08はコサイン往復の折り返しに置くため、止まった姿がそれぞれ別の性格を見せます。このように止まり方そのものが項目の性格を写すので、動きを減らした設定で開いてもページはただの静的な絵にはならず、各項目の代表的な形が一枚ずつ並んだ見本帳のように読めます。

この編には3つのバリエーションも用意されています。1つ目は01の指数上限をSUPER_N 4.0から8.0まで広げ、折り返しの終着点をより角ばった形に押し広げます。2つ目は02の角度上限をBEVEL_DEG 18度から36度まで広げ、斜面幅がさらに大きく育つようにします。3つ目もまた01が対象で、SWEEP_MSを2400msから1200msへ縮め、指数が円と四角のあいだを往復する速さを2倍にします。3つとも定数の値を差し替えるだけであり、計算層の関数そのものは1行も変わらないため、元の9項目を読んだ人には同じコードの別の調律に見えます。

指数値・角度値・比率値という3つの軸は、9項目それぞれの計算層がどんな入力を受け取るかで分かれています。スーパー楕円のように曲線の式そのものへ数値を渡す項目もあれば、ベベルのように三角比を経由してから半径へ変換する項目、モーフィングのように2つの固定形状のあいだの位置だけを渡す項目もあり、同じ corner-shape という性質でも値の意味は項目ごとに異なります。

同じ数値でも、軸が変われば角の曲線に届くまでの道のりが変わります。指数は式の形そのものを連続的に変え、角度は三角比を通ってから2つの半径に分かれ、比率は動かない2つの端点のあいだの位置だけを運びます。だから、値の単位が同じ px や度数であっても、角がどう変わるかは計算層が通る関数が決めます。03のスクープは押している時間という入力を曲げて凹みの深さに替え、07のはしごは時間を5段の固定値へ量子化してから補間するなど、入力の種類はさらに分かれます。05と08は時計の位相そのものを入力にするため、ユーザーが値に触らずにも曲線が動き続けます。02の斜面幅は角度から三角比で導かれる従属値なので、指で触っているのは角度の1つだけで、幅のほうは計算が連れてきます。

軸が違えば、値を動かしたときにユーザーの目に映る変化の場所も自然と変わります。

3つの項目は、それぞれ全く異なる基準に反応しています。01の稜線は指数値に反応します — 同じ40px半径でも、指数1では円に近く、4.0ではスクワークルになるため、角がどれだけ四角く読めるかは指数だけが決めます(css corner shape squircle の最も純粋な形がこれです)。02の面は角度値に反応します — BEVEL_RY は14pxのまま固定で、Math.tan が横軸半径だけを広げるため、斜面幅は角度のタンジェントに従い、角度を2倍にすれば幅は2倍を超えて広がります。06の輪郭は比率値に反応します — round という形も bevel という形も自分では動かず、その2つのあいだを移動する補間比率だけが動くため、同じカードに2つの固定した端点と1つの旅する値が乗っています。9項目は1つのzipにまとまっており、開くパスワードはx4zdvxz7のひとつだけで、パターンごとに別のパスワードを探す必要はありません。

このcss squircle border radius の集まりは、既出の3つの記事の隣に置けます。一行でコピペOK ― CSS カード ホバー アニメーション9選はプロパティ1つでカード全体を持ち上げますが、ここでは06がカードの位置ではなく角の曲線をモーフィングさせます。【コピペOK】css バッジ デザイン 9選 — NEW・SALE・認証バッジはバッジの形を静的な構成として作る一方、02は1つのベベル面を生きた値に変えます。プロフィール UI デザイン 9種 — 画像ファイルなしはブロック全体を部品から組み立てる記事で、09のインセットフレームはそのようなブロックが持てる縁の層です。

どこで壊れるか — 非対応ブラウザ、フォールバック、reduced-motion

css corner-shape のデモがどこで滑らかさを失うか、3つの場所で見えます。

プロパティがなくなり、近似が引き継ぐとき

9項目はどれも起動時に一度だけ CSS.supports を確認し、確認が偽なら border-radius の近似を書き込みます — 01は指数が大きいほど半径を狭め、03と04は凹んだスクープと掘られたノッチを同じ大きさの丸い角と入れ替えます。フォールバックでも輪郭は動き続けますが、性格は変わります。スクープは凸に、ノッチは丸に読めるため、border-radius 経由で見る css スクワークル は本物の形の親戚であって形そのものではありません。08はまさにその差を画面に置くために存在し、確認が偽のとき左のキャプションは「非対応フォールバック」に自分で書き換わるため、対応なしのブラウザでも比較は本当のことを語り続けます。

スプリングの途中で次のタブ切り替えが来るとき

04のノッチ深さは切り替えのあと SPRING_MS のあいだだけスプリングを追い、鳴り終わっていないスプリングの途中に次の切り替えが来ると、現在の深さからブレンドするのではなく新しい phaseStart で時計をやり直します — 溝は新しいタブへ飛んで、曲線の最初からもう一度鳴ります。

reduced-motion は各項目をそれぞれ違う状態で止める

9項目はそれぞれ独自の prefers-reduced-motion 分岐を持ち、その内容は項目ごとに異なります。01は指数を SUPER_N に固定し、02は角度を BEVEL_DEG に固定し、03はスクープ半径を即座に SCOOP_MAX へ適用し、04はスプリングなしでノッチ深さをまっすぐ NOTCH_MAX へ置き、05は波の位相を固定された t で凍らせ、06は比率をホバー目標へまっすぐ設定し、07は最上段へ一気に上がり、08は t を 0.5(コサイン掃引の R_MAX 状態)に固定し、09はインセットを INSET_MAX で固定します。Sass 側でも、同じメディアクエリの下で全項目に transition: none が対になっています。

3つのバリエーションとアクセシビリティ

バリエーション 変わるもの 変わらないもの
スクワークル指数を上げる 01 SUPER_N 4.0 → 8.0 MIN_N、SWEEP_MS、40px半径
ベベル角度を広げる 02 BEVEL_DEG 18 → 36 MIN_DEG、BEVEL_RY、BEVEL_MS
指数の掃引を速くする 01 SWEEP_MS 2400 → 1200 SUPER_N、MIN_N、プリセット一覧

01の SUPER_N を4.0から8.0へ上げると、ソースは指数を MIN_N(1)から上限まで滑らかな補間で流すため、流れが届く稜線はさらに四角形側へ深くなり、上限を変えるだけで流れ全体の質感が変わります。02の BEVEL_DEG を18から36へ上げると、横の深さは BEVEL_RY × tan(角度) で計算され斜面幅は hypot で測られるため、角度を2倍にすれば斜面幅は2倍を超えて広がります。01の SWEEP_MS を2400msから1200msへ縮めると、ソースは経過時間を SWEEP_MS で割った三角波で指数を流すため、円と四角のあいだを行き来する流れがちょうど2倍速くなります。

入力の側では、9つのファイルを grep するとリスナーは3通りに分かれます。01と02は click だけを聞きます。04・05・07・08は click と keydown を組み合わせます。03は押し続けを追うため keydown・keyup に pointerdown・pointerup・pointercancel・pointerleave を加え、06は focus・blur・click とともに pointerenter・pointerleave を読み、09は keydown・keyup と pointerdown・pointerup・pointerleave を配線します。すべてのボタンは読み上げ可能なラベルかテキスト内容を持ち、カードの下の数値は aria-live="polite" の段落なので、値が変わるたびに読み上げられます。01・02・03・05・06・09の切り替え式ボタンは状態を aria-pressed で公開し、04のタブは代わりに aria-selected を使います。7つの項目では自動巡回は is-demo クラスがあるあいだ、かつ reduced-motion が無効のあいだだけ走り、最初の実際のポインターやキーの入力がそのクラスを外すため、訪問者が操作を引き受けたあとに値は巡回をやめます。05と08の波とコサインは時計を止めない限り動き続けますが、クリックで一時停止できるため、訪問者が主導権を取り戻す道は9項目とも同じです。

FAQ

css corner shape はどこでも動きますか、それともこの css corner shape 例 にはフォールバックが必要ですか

各項目は起動時に同じ1行の確認 — CSS.supports に corner-shape のペアを渡す — を実行し、失敗すれば border-radius の近似を書き込みます。対応する環境ではプロパティそのものが使え、どこでもなければ、輪郭は同じ数値のまま近似として動き続けます。08は2つのレンダリングを並べて、その違いを説明ではなく目で見える形にしています。

この css squircle border radius のパターンは、それぞれ別ファイルですか、それとも設定違いの1コンポーネントですか

9つとも独立したファイルです。項目ごとに自分自身の定数と apply 関数を持ち、上のコード欄はテンプレートに数値を差し込んだ出力ではなく、そのファイルから直接抜き出した抜粋です。zipには9つの html・scss・js ファイルが一緒に入っているため、他の8つを引き連れずに1つのパターンだけ持ち上げられます。

08の近似ペインは、なぜポリフィルではなくフォールバックと呼ぶのですか

プロパティがない場所にそれをインストールしないからです。このペインは、ネイティブのレンダリングの隣に border-radius の代役を描いて比べるだけです。本物のポリフィルは角の曲線そのものを合成しなければなりませんが、このペインは、そのプロパティを持たないブラウザが同じ半径値に対して何を描くかを示すだけです。

Sources: MDN — corner-shape · MDN — border-radius · MDN — superellipse() · MDN — @supports

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

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