Angularでロードバイクの「サドル高計算アプリ」を作ってみた

ロードバイクに乗っていると必ずぶつかるのが「サドルの高さ、これで合ってるんだっけ?」問題。ショップで測ってもらった数値をなんとなく覚えているだけ、というライダーも多いのではないでしょうか。

今回、股下長とクランク長を入力するだけでサドル高を計算してくれるシンプルなツールを、Angularのスタンドアロンコンポーネントとして作ってみました。この記事では、作ったものの構成と、途中で追加していった機能を順を追って紹介します。

作ったもの

  • 3つの計算方式(ルーム式・ハンター・アレン式・109%法)から選んでサドル高を算出
  • 計算結果を自転車のイラストで視覚化(サドルの位置が実際に上下する)
  • 膝の曲がり角度を入力して、さらに数mm単位で微調整できる機能

スマホでサクッと使えるように、モバイルファーストのシンプルなUIにしています。

技術構成

Angularのstandaloneコンポーネント1本で完結する、小さめの構成です。

@Component({
  selector: 'app-saddle-calculator',
  standalone: true,
  imports: [FormsModule, CommonModule],
  templateUrl: './saddle-calculator.html',
  styleUrl: './saddle-calculator.css',
})
export class SaddleCalculator {
  // ...
}

FormsModule を使った双方向バインディング([(ngModel)])で入力欄を管理し、CommonModule*ngIf / *ngFor を使うだけの、わりとオーソドックスな作りです。テストは Angular CLI が生成する標準的な spec ファイルのままにしてあります。

1. 計算式は3方式から選べるように

サドル高の計算式にはいくつか流派があります。今回は以下の3つを実装しました。

方式係数特徴
ルーム式(LeMond method)股下 × 0.883ロードバイクで最も一般的な基準
ハンター・アレン式股下 × 0.885ルーム式よりわずかに高め
109%法股下 × 1.09別の基準点から算出する方式
methods = [
  { id: 'lemond', name: 'ルール・ルム式', description: 'ロードバイクで一般的な基準', coefficient: 0.883 },
  { id: 'allen', name: 'ハンター・アレン式', description: '少し高めの設定になる傾向', coefficient: 0.885 },
  { id: '109', name: '109%法', description: '股下から比率で算出', coefficient: 1.09 },
];

calculate() {
  // ...
  const method = this.methods.find((m) => m.id === this.calculationMethod);
  let height = this.inseam * method.coefficient;

  // クランク長補正(170mmを基準に、長い/短い分だけ調整)
  height -= (this.crankLength - 170) / 10;

  this.saddleHeight = Math.round(height * 10) / 10;
}

クランク長が標準の170mmより長ければ少し低めに、短ければ少し高めに補正する処理も入れています。UI側は方式をカード形式で並べて、タップで選べるようにしました。

<div class="method-list">
  <button
    type="button"
    class="method-card"
    *ngFor="let method of methods"
    [class.active]="calculationMethod === method.id"
    (click)="calculationMethod = method.id"
  >
    <div class="method-name">{{ method.name }}</div>
    <div class="method-description">{{ method.description }}</div>
  </button>
</div>

ちなみに、この3方式以外にも「ヒール・トゥ・ペダル法」(実際にまたがって調整する方法)や、後述する膝角度を使った方法など、精度の異なるやり方がいくつも存在します。式だけだと股下という単一の指標しか見ないので、個人差(大腿骨とすねの長さの比率など)までは反映しきれない、という限界もあります。

2. 計算結果を自転車イラストで見える化する

数値だけポンと出されても「で、それがどのくらいの高さなの?」というのがピンと来ないので、計算結果を自転車のイラストに反映させることにしました。

ポイントは、フレームの「シートチューブ」は固定にして、そこから伸びる「シートポスト」だけを可変にしたことです。実際のロードバイクでサドル高を調整するのはシートポストの引き出し量なので、これは見た目だけでなく仕組み的にも実車に近い表現になっています。

<!-- シートポスト(サドル高に応じて伸縮) -->
<rect
  x="116.5"
  width="3"
  rx="1.5"
  [attr.y]="seatTopY"
  [attr.height]="100 - seatTopY"
  class="seat-post"
/>

<!-- サドル -->
<ellipse cx="118" rx="24" ry="3.5" [attr.cy]="seatTopY - 3" class="saddle" />

seatTopY は計算結果(cm)をSVG座標に変換した値で、専用のメソッドで算出しています。

private mapHeightToY(heightCm: number): number {
  const minHeight = 60;
  const maxHeight = 100;
  const bottomY = 96; // サドル高が低いときのY座標
  const topY = 45; // サドル高が高いときのY座標

  const clamped = Math.min(Math.max(heightCm, minHeight), maxHeight);
  const ratio = (clamped - minHeight) / (maxHeight - minHeight);

  return bottomY - ratio * (bottomY - topY);
}

ここで少しハマったのが、SVGの <line> 要素はCSSの transition でうまくアニメーションしてくれないという点です。x1/y1/x2/y2 はCSSのアニメーション対象になっていないブラウザが多く、値を変えても瞬時にジャンプしてしまいます。そこで、伸縮させたい部分は <line> ではなく <rect> にして、yheight をアニメーションさせる形に変更しました。rectx/y/width/height はCSSの幾何プロパティとして扱われるため、こちらは問題なく滑らかに動きます。

.seat-post {
  fill: #555555;
  transition: y 0.6s ease, height 0.6s ease;
}

再計算するたびに、シートポストがスッと伸び縮みするちょっとした演出になりました。

ロードバイクらしさを詰める

最初に描いたイラストは、率直に言うと「ロードバイクっぽくない」というフィードバックをもらいました。そこから何度か手直ししています。

  • フラットな印象だったハンドルを、ステム+曲線パスでドロップハンドルらしい形状に
  • 太かったタイヤを、タイヤとリムの二重線にして細身のロードタイヤ
  • ホイールサイズを1.5倍に拡大
  • ヘッドチューブを短くして、フロント周りを引き締めた印象に
  • トップチューブを地面と水平になるよう座標を調整

このあたりは数値をひたすら微調整しながら「それっぽさ」を詰めていく、地道な作業でした。

3. 膝角度による微調整機能

式ベースの計算はあくまで目安なので、もう一歩踏み込んだ調整方法として、膝の曲がり角度を使った微調整機能を追加しました。

やり方はシンプルで、クランクを6時位置(ペダル最下点)にした状態で、股関節・膝・かかとを結んだ線の膝の曲がり具合(角度)を測ります。目安は25〜35度で、これより角度が大きい(膝が伸びきっていない)ならサドルが低すぎ、小さい(伸びすぎ)なら高すぎる、というのが一般的な考え方です。

private readonly KNEE_ANGLE_MIN = 25;
private readonly KNEE_ANGLE_MAX = 35;
private readonly KNEE_ANGLE_TARGET = 30;
private readonly MM_PER_DEGREE = 1; // 経験則:膝角度1度の差 ≈ サドル高1mm

adjustByKneeAngle() {
  if (this.kneeAngle == null || this.kneeAngle <= 0) {
    this.kneeAdjustmentMessage = '膝の角度を入力してください。';
    this.kneeAdjustmentMm = 0;
    return;
  }

  if (this.kneeAngle >= this.KNEE_ANGLE_MIN && this.kneeAngle <= this.KNEE_ANGLE_MAX) {
    this.kneeAdjustmentMm = 0;
    this.kneeAdjustmentMessage = '適正範囲内です。今の高さのままで問題ありません。';
    return;
  }

  const diff = this.kneeAngle - this.KNEE_ANGLE_TARGET;
  this.kneeAdjustmentMm = Math.round(diff * this.MM_PER_DEGREE);

  if (this.kneeAdjustmentMm > 0) {
    this.kneeAdjustmentMessage = `膝が伸びきっていないようです。サドルを約 ${this.kneeAdjustmentMm} mm 上げてみてください。`;
  } else {
    this.kneeAdjustmentMessage = `膝が伸びすぎているようです。サドルを約 ${Math.abs(this.kneeAdjustmentMm)} mm 下げてみてください。`;
  }
}

「膝角度1度の差 ≈ サドル高1mm」というのは、いくつかのバイクフィッティング関連の記事にあった実例(40度を30度まで下げるのに10mmサドルを上げた、という報告)を参考にした簡易的な近似です。あくまで目安であり、正式なバイクフィッティングにおけるゴニオメーター計測やモーションキャプチャによるダイナミックフィッティングほどの精度はありません。

また、基本の計算(股下・クランク長)をやり直したときに、古い微調整結果が残ってしまうと紛らわしいので、calculate() の中でリセットするようにしています。

calculate() {
  this.errorMessage = '';

  // 基本計算をやり直すたびに、膝角度の微調整結果はリセットする
  this.kneeAngle = null;
  this.kneeAdjustmentMm = 0;
  this.kneeAdjustmentMessage = '';

  // ...
}

測り方を説明する図をつける

「膝の角度を測ってください」と文章だけで言われても分かりにくいので、測定方法を示すイラストも追加しました。ポイントは以下の3つです。

  1. クランクを6時位置(ペダル最下点)に合わせる
  2. 股関節・膝・かかとを結んだラインを側面から見る
  3. 「脚がまっすぐ伸びた場合」の点線と、実際のすね(膝から下)の間にできる隙間の角度を測る

この図を作る過程で、自分でも勘違いしていた点がありました。角度の弧は「太もも」と「すね」の間(=膝関節そのものの内角)ではなく、「まっすぐ伸ばしたと仮定した場合の点線」と「実際のすね」の間に描く必要がある、ということです。前者だと見た目の角度が全然違う(かなり広い角度になってしまう)ので、実際に測っている数値とズレて見えてしまいます。

また、自転車全体のイラストを自転車の右側から見た構図で描いていたので、膝が曲がる向き(前方=画面右側に張り出す)もそれに合わせて反転修正しました。

<div class="knee-diagram">
  <svg viewBox="220 60 440 380" class="knee-diagram-svg">
    <!-- 点線:脚がまっすぐ伸びた場合の参照線 -->
    <line x1="320" y1="230" x2="396.6" y2="383.1" class="dashed-ref" />

    <!-- 太もも・すね -->
    <line x1="250" y1="90" x2="320" y2="230" class="leg-line" />
    <line x1="320" y1="230" x2="300" y2="400" class="leg-line" />

    <!-- 角度を示す弧(点線とすねの間) -->
    <path d="M333.4,256.8 A30,30 0 0,1 316.5,259.8" class="angle-arc" />
    <text x="332" y="284" class="theta-label">θ</text>

    <!-- クランク・ペダル -->
    <line x1="300" y1="340" x2="300" y2="400" class="crank-line" />
    <circle cx="300" cy="340" r="8" class="pivot" />
    <ellipse cx="300" cy="400" rx="16" ry="6" class="pivot" />
  </svg>
</div>

図のサイズ感については、viewBox を実際に描かれている範囲ギリギリまで切り詰めることで、同じ表示幅でもコンテンツが大きく見えるように調整しました。余白が多い viewBox は、その分だけ描画内容が相対的に小さく表示されてしまいます。

- <svg viewBox="0 0 680 480" class="knee-diagram-svg">
+ <svg viewBox="220 60 440 380" class="knee-diagram-svg">

モバイルUIまわりの細かい工夫

サドル高計算はショップの店先や自宅で、スマホからサクッと使うシーンを想定しているので、以下のような配慮を入れています。

  • inputfont-size を18px(小さい画面では16px)にして、iOSでの自動ズームを防止
  • touch-action: manipulation でタップ時の遅延・ズームを抑制
  • ボタンやカードの高さを48px以上確保して、指でタップしやすいサイズに
  • button:activetransform: scale(0.98) を入れて、タップした感触を出す
input,
button {
  touch-action: manipulation;
}

button:active {
  transform: scale(0.98);
}

できあがったもの、そして今後

最終的に、以下のような一連の機能を持つツールになりました。

  • 3方式から選べるサドル高計算
  • 計算結果に応じて動く自転車イラスト(シートポストが伸縮)
  • 膝角度による微調整と、その測り方を示す図解

作りながら話していた「もっと面白くできないか」というアイデアの中では、3方式を同時比較表示する機能や、調整履歴を保存してグラフ化する機能などもまだ手つかずのままです。機会があれば、そのあたりも次回追加していきたいと思います。


本記事のコードはAngularのstandaloneコンポーネントを前提にしています。SVGアニメーションまわり(特に<line>要素のCSSトランジションが効かない問題)は地味にハマりやすいポイントなので、同じような可視化を作る方の参考になれば幸いです。

Comments

No comments yet. Why don’t you start the discussion?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です