スキップしてメイン コンテンツに移動

transform が反映されない Firefox 3.5~11 用の getBoundingClientRect の polyfill

Firefox 3.6 で実行中のテストページ、いずれ公開します

Gecko エンジンは、1.9.0(=Firefox 3.0) から getBoundingClientRect() をサポートし、続いて 1.9.1(=Firefox 3.5) から -moz-transform をサポートします。

しかし、getBoundingClientRect() の戻り値である DOMRect の各値が transform を反映していない問題がありました。これは Firefox 12 で解消する――らしい(caniuse による)。

caniuse の情報は必ずしも当てにならないところ、まだ実ブラウザで確認していません。当てにならないというのは、例えばこの時代の Firefox は、DOMRect.x, DOMRect.y をサポートしないが、caniuse ではサポート済みになっていたりします。

さて、検索したところ先行する polyfill は見つからなかった。Firefox 3.5(2009年7月30日) から 12(2012年4月24日) までは3年弱。この間、ウェブ開発者はどうしていたのだろう…?(例えば closure-library / closure / goog / style / style.js では、重視している形跡がない。)

getBoundingClientRect polyfill:Firefox 3.5~11 の問題を回避する

昨日から本日の午前中までで、この polyfill を Gemini さんに頑張ってもらって完成させた。変形にまつわるプログラミングは僕の手に余るので、ダメ出しと、ブラウザでのテスト実施・目視確認、最適化の助言に徹することになった。

中でも、複数の祖先要素に適用された変形を累積しなくてはならず、これには Gemini さんも大分苦戦していた。しかし、何度目かの試行で答えを出してくれた。最終的に出来上がったコードは美しいもので、とてもエキサイティングな時間を過ごすことが出来た。ペアプログラミングの相棒として、敬意を抱かざるを得ない。

polyfill は Firefox 3.6 と10で動作確認済です。

/**
 * @preserve getBoundingClientRect Polyfill for Gecko 1.9.1 to 11 (Firefox 3.5 ~ 11)
 * https://github.com/itozyun/snowdaifuku
 *
 * Released under the MIT License.
 * Copyright (c) 2026 itozyun & Gemini (An AI-Human Collaboration Production)
 *
 * [Overview]
 * Fixes a critical legacy bug in early Gecko engines where Element.prototype.getBoundingClientRect()
 * completely ignored CSS3 2D Transforms (-moz-transform), returning un-transformed layout dimensions.
 *
 * [Architectural Highlights]
 * 1. Zero-DOM-Mutation Architecture: Exploits Firefox 11's inherent bug of returning pre-transform 
 *    coordinates to gather raw geometric data WITHOUT heavy layout-thrashing 'none' style injection.
 * 2. Strict Micro-Optimization: Features ultra-fast tail-eliminated loop constructs, fully localized 
 *    lexical scopes for native globals, and completely unrolled vertex-mapping pipelines.
 * 3. Topological Cascade Defense: Filters out non-transformed ancestors at the ingress traversal phase, 
 *    erasing loop friction and object allocation overheads down to O(K) where K is the actual transform depth.
 *
 * @param {!Element} targetElement
 * @return {!DOMRect}
 */
function getBoundingClientRectForGecko1_9_1to11(targetElement) {
  // 高速ルックアップマップ: キーワード表記の原点を速やかに割合(%)へと正規化
  var keywordMap = { 'left': '0%', 'right': '100%', 'top': '0%', 'bottom': '100%', 'center': '50%' };

  var _getComputedStyle = getComputedStyle;

  // 自分自身の変形前の生座標を最初に取得 (Firefox 11の実環境ではこれが「変形前」の正しい初期値となる)
  var selfRect = targetElement.getBoundingClientRect();

  var current = targetElement;
  var chain = [];
  var hasTransform = false;

  // --- 1. イングレス・フェーズ: 親要素まで遡り、変形が「実際にかかっている要素だけ」を厳選ストック ---
  while (current && current.nodeType === 1) {
    var style = _getComputedStyle(current, null);
    var transform = style.MozTransform; // Firefox 3.5~11専用のため、プレフィックスを直撃させて最速化
    
    // トランスフォームが有効な要素のみに絞る。無変形の親要素を配列に入れないことでメインループの回転数を劇的に削減
    if (transform && transform !== 'none') {
      hasTransform = true;
      chain.push({
        transform: transform,
        style: style,
        // 三項演算子による最適化: 自身なら取得済みのオブジェクトを再利用し、余計な関数呼び出しコストを削減
        baseRect: current === targetElement ? selfRect : current.getBoundingClientRect()
      });
    }
    current = current.parentNode;
  }

  // 先祖を含めトランスフォームがどこにもかかっていなければ、ネイティブの生座標をそのまま返して瞬時に脱出
  if (!hasTransform) {
    return selfRect;
  }

  // ネイティブのグローバル関数・数式メソッドのローカルエイリアス化(圧縮率とスコープ解決速度を極大化)
  var _parseFloat = parseFloat,
      _mathMin = Math.min,
      _mathMax = Math.max;

  // --- 2. ローカル展開フェーズ: 自身の変形前の4隅の「画面絶対座標」を変数へばらしてプロパティアクセスを抹殺 ---
  var sLeft   = selfRect.left,
      sTop    = selfRect.top,
      sWidth  = selfRect.width,
      sHeight = selfRect.height;

  // 初期値としての4隅の絶対点を定義 (左上・右上・右下・左下の順)
  var x0 = sLeft,          y0 = sTop,
      x1 = sLeft + sWidth, y1 = sTop,
      x2 = sLeft + sWidth, y2 = sTop + sHeight,
      x3 = sLeft,          y3 = sTop + sHeight;

  // --- 3. 幾何学トランスフォームフェーズ: 厳選された要素だけで、4隅の点そのものを子から親へ順に変形 ---
  // 高速ループ構文: `nodeInfo = chain[++i]` により配列終端の undefined 検知で自動終了させ、比較演算コストをゼロ化
  for (var i = -1, nodeInfo; nodeInfo = chain[++i];) {
    // "matrix(a, b, c, d, tx, ty)" から数値部分のみを切り出す高速パース
    var matrixValues = nodeInfo.transform.split('matrix(')[1];

    // カンマで直接分割。parseFloatの特性(後続の文字を無視)を利用し閉じカッコ ')' を除去する処理すらオミット
    var m = matrixValues.split(','),
        a = _parseFloat(m[0]), b = _parseFloat(m[1]), c = _parseFloat(m[2]),
        d = _parseFloat(m[3]), tx = _parseFloat(m[4]), ty = _parseFloat(m[5]);

    // トランスフォームの回転・拡大縮小の軸となる中心点(Origin)の解析
    var nodeStyle = nodeInfo.style;
    var origin = nodeStyle.MozTransformOrigin,
        originValueX = '50%', originValueY = '50%', originValues;

    if (origin) {
      originValues = origin.split(' ');
      originValueX = originValues[0];
      originValueY = originValues[1] || '50%'; // Y方向が省略されている場合は center (50%) 扱い
    }

    // キーワード表記(center 等)を高速ルックアップテーブルで割合に置換
    originValueX = keywordMap[originValueX] || originValueX;
    originValueY = keywordMap[originValueY] || originValueY;

    var originX = _parseFloat(originValueX),
        originY = _parseFloat(originValueY);

    var nRect  = nodeInfo.baseRect,
        nLeft  = nRect.left,
        nTop   = nRect.top,
        nWidth = nRect.width,
        nHeight= nRect.height;

    // Origin が % 表記だった場合、要素の変形前の生サイズを元にピクセル(px)の絶対値へマッピング
    if (originValueX.indexOf('%') !== -1) originX = nWidth  * originX / 100;
    if (originValueY.indexOf('%') !== -1) originY = nHeight * originY / 100;

    // --- [行列演算のコア・解説] ---
    // CSSのトランスフォーム行列は、要素自身の「変形前のOrigin(中心)」を局所的な原点 (0, 0) として作用する。
    // そのため、画面上のグローバルな絶対座標の点(x, y)に対して行列を正しく適用するためには以下の3ステップを踏む。
    // 
    // 1. 【原点移動(平行移動の逆)】: `lx = x - ox`
    //    画面絶対座標にある 4 隅の点から、現在の変形要素の Origin の絶対位置(ox, oy)を差し引く。
    //    これにより、点を「この変形要素の Origin を原点 (0,0) としたローカル空間」へと一時的に引きずり込む。
    // 
    // 2. 【2D変換行列の乗算】: `nx = lx * a + ly * c`, `ny = lx * b + ly * d`
    //    ローカル空間に移した点に対し、アフィン変換行列 [a, c / b, d] を掛け合わせる。
    //    これにより、回転(rotate)・拡大縮小(scale)・傾き(skew)による空間の歪みが点に正しく加算される。
    // 
    // 3. 【復元と移動の一発統合】: `x = nx + tx + ox`
    //    変形し終わったローカルの点に対し、行列由来の移動量(tx, ty)を加え、さらにステップ1で引いていた
    //    Originの位置(ox, oy)を足し戻すことで、再び画面全体の絶対座標系(ビューポート基準)へと復帰させる。
    //    この `tx + ox` をループの直前で `baseX / baseY` として事前に括り出すことで、ループ内の加算回数を削減。
    
    var ox = nLeft + originX;
    var oy = nTop  + originY;

    var baseX = tx + ox,
        baseY = ty + oy;

    // 完全展開された頂点トランスフォーム・パイプライン (1頂点あたり4回、計16回分の加算処理を削減)
    var lx0 = x0 - ox, ly0 = y0 - oy;
    x0 = (lx0 * a + ly0 * c) + baseX; y0 = (lx0 * b + ly0 * d) + baseY;

    var lx1 = x1 - ox, ly1 = y1 - oy;
    x1 = (lx1 * a + ly1 * c) + baseX; y1 = (lx1 * b + ly1 * d) + baseY;

    var lx2 = x2 - ox, ly2 = y2 - oy;
    x2 = (lx2 * a + ly2 * c) + baseX; y2 = (lx2 * b + ly2 * d) + baseY;

    var lx3 = x3 - ox, ly3 = y3 - oy;
    x3 = (lx3 * a + ly3 * c) + baseX; y3 = (lx3 * b + ly3 * d) + baseY;
  }

  // --- 4. エグレス・フェーズ: 変形・射影し終わった4つの絶対点から、最外形(バウンディングボックス)を生成 ---
  var left   = _mathMin(x0, x1, x2, x3),
      right  = _mathMax(x0, x1, x2, x3),
      top    = _mathMin(y0, y1, y2, y3),
      bottom = _mathMax(y0, y1, y2, y3);

  // 標準の DOMRect 互換オブジェクトをビルドして返却
  return {
    top: top, left: left, right: right, bottom: bottom,
    width: right - left, height: bottom - top, x: left, y: top
  };
}

prototype のメソッド差し替えが出来なかった件

残念ながら Firefox 10 の getBoundingClientRect() は Function インスタンスでは無いようで、prototype を書き替えるテクニックが使えなかった。

var originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;

// Firefox 10 では、エラーで動かなかった。Native メソッドで call() が使えないようだ。
rect = originalGetBoundingClientRect.call(elm);

Element.prototype._originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;

// Firefox 10 ではこちらもエラーで動かなかった。
rect = elm._originalGetBoundingClientRect();

この他にもある getBoundingClientRect() の問題

さて、caniuse によると getBoundingClientRect() にはこの他にも polyfill が必要だ。加えて canuse に未収録の IE 限定のバグもあるという。

left, top が常に 2px 多いのは IE5 の話で、IE5.5 以上では 2px ずれていませんでした。

ただし、IE6 以降でも parentNode が document.body の場合は x, y から -2px 引く必要があります。 IE5+, Firefox3+, Opera9.5+ で getBoundingClientRect() は使用可能ですが、-2px 引く必要があるのは IE だけです。

いずれにしても、transform にまつわるものに比べたら大した問題ではない。


心から Gemini さんの健闘を称えたい。久しぶりに、心が洗われるような見事な仕事を視た心地です。