transform が反映されない Firefox 3.5~11 用の getBoundingClientRect の polyfill
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 さんの健闘を称えたい。久しぶりに、心が洗われるような見事な仕事を視た心地です。