🌟
逆線形補間(unlerp)解説
対象:私の様に数学が苦手でもOK。Vue / React / Nuxt / Next / TypeScript / JavaScript でUI座標を扱う人
ゴール:unlerpで「いま区間の何%か」を出せるようになる(=px → % の橋渡しができる)
TL;DR(最短結論)
-
unlerpは「区間
[a, b]の中で値xがどれくらい進んだか」を 0〜1 で返す関数
公式:t = (x - a) / (b - a) - UIでは ビューポートのpx → 親コンテナ内の% に直すのに超便利
-
getBoundingClientRect()と組み合わせるとスクロールにも強い
unlerp とは?
- 物差しを思い浮かべましょう。0〜100cm のうち 23cm は 23%。式にすると
23 / 100 = 0.23。 - 一般化すると:
(今の位置 - 始点) / (終点 - 始点)→ 0〜1 の割合
基本関数
/** 区間 [a, b] の中で x が何割(0〜1)進んだかを返す */
export function unlerp(x: number, a: number, b: number): number {
return (x - a) / (b - a);
}
-
/** ... */… 何をする関数かコメント(自分や未来の自分のため) -
export function unlerp(...)… 他ファイルからも使えるように公開 -
x: number… 今いる場所(測りたい値) -
a: number, b: number… 始点と終点(左端と右端など) -
return (x - a) / (b - a);… 「始点からどれだけ進んだか」を区間長で割る=0〜1 の割合
注意:
a === b(区間長が0)のときは ゼロ除算になるので、実戦では安全版を使います↓
安全版:0除算と範囲外クランプ
/** 0〜1に丸める安全版 unlerp(a===b回避 & 範囲外は切り詰め) */
export function unlerp01(x: number, a: number, b: number): number {
if (a === b) return 0;
const t = (x - a) / (b - a);
return Math.min(1, Math.max(0, t));
}
-
if (a === b) return 0;… 区間長が0なら 0 を返す(好みで 0.5 にしてもOK) -
const t = ...… まず素の割合を計算 -
Math.min(1, Math.max(0, t))… 0〜1にクランプ(マーカーを枠内に収めたいとき便利)
実用ユーティリティ:点(px, py) → 親Rect内の%
/** 点(px,py)を親Rect内の%へ(オフセット微調整つき) */
export function toPercentInRect(
px: number, py: number, rect: DOMRect,
offsetXPct = 0, offsetYPct = 0, clamp = true
) {
const tX = rect.width ? (px - rect.left) / rect.width : 0;
const tY = rect.height ? (py - rect.top) / rect.height : 0;
let xPct = tX * 100 + offsetXPct;
let yPct = tY * 100 + offsetYPct;
if (clamp) {
xPct = Math.min(100, Math.max(0, xPct));
yPct = Math.min(100, Math.max(0, yPct));
}
return { xPct, yPct };
}
-
px, py… ビューポート基準の座標(getBoundingClientRect()から求める点など) -
rect: DOMRect… 親コンテナの Rect(同じくビューポート基準) -
offsetXPct/offsetYPct… 最後に %単位で微調整するつまみ(見た目合わせが楽) -
clamp = true… 0〜100%に収めるかどうかのフラグ -
rect.width ? ... : 0… 幅0/高0(未レイアウト)対策 -
tX, tY… unlerp で 0〜1 の割合に変換 -
*100 + offset… %に直し、微調整を加算 -
clamp… 0〜100%に切り詰め(必要なときだけ) -
return { xPct, yPct }… CSSにそのまま渡せる形で返す
実例:アンカー点 → 親%
前提(すべて getBoundingClientRect() の値=ビューポート基準)
- 親Rect:
left=100, top=200, width=800, height=600 - 子Rect:
left=220, top=260, width=300, height=150 - アンカー:
anchorX=0.35, anchorY=0.25(子の左35%, 上25%) - オフセット:
offsetXPct=-2, offsetYPct=+1
- 子アンカーの画面座標
const px = 220 + 300 * 0.35 = 325;
const py = 260 + 150 * 0.25 = 297.5;
- 親内での割合(unlerp)→ %
x% = ((325 - 100) / 800) * 100 = 28.125%;
y% = ((297.5 - 200) / 600) * 100 = 16.25%;
- 微調整
left = 28.125 - 2 = 26.125%;
top = 16.25 + 1 = 17.25%;
スクロールしても 親と子を同じ基準(ビューポート)で差分を取るため、相殺されて安定します。
Vue 3(Composition API)
import { ref, onMounted, nextTick } from "vue";
const leftPct = ref(0);
const topPct = ref(0);
onMounted(async () => {
await nextTick(); // DOMサイズが確定するのを待つ
const parent = document.querySelector(".container") as HTMLElement;
const child = document.querySelector(".child") as HTMLElement;
if (!parent || !child) return; // 念のための存在チェック
const pr = parent.getBoundingClientRect(); // 親Rect(ビューポート基準)
const cr = child.getBoundingClientRect(); // 子Rect(同上)
// 子の中央点(px,py)をとる
const px = cr.left + cr.width * 0.5;
const py = cr.top + cr.height * 0.5;
// 親内の%へ(微調整は例として -2%, +1%)
const { xPct, yPct } = toPercentInRect(px, py, pr, -2, +1);
leftPct.value = xPct; // リアクティブに反映
topPct.value = yPct;
});
-
nextTick()… 描画完了後にRectを読む(0×0回避) -
getBoundingClientRect()… 見た目基準の位置・サイズ(transform後の寸法にも対応) -
toPercentInRect()… unlerp → % → 微調整をひとまとめに
テンプレ側:
<div class="marker" :style="{ left: leftPct + '%', top: topPct + '%' }"></div>
React 版(useEffect)
import { useEffect, useRef } from "react";
export function Marker() {
const markerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const parent = document.querySelector(".container") as HTMLElement;
const child = document.querySelector(".child") as HTMLElement;
if (!parent || !child || !markerRef.current) return;
const pr = parent.getBoundingClientRect();
const cr = child.getBoundingClientRect();
const px = cr.left + cr.width * 0.5;
const py = cr.top + cr.height * 0.5;
const { xPct, yPct } = toPercentInRect(px, py, pr);
markerRef.current.style.left = `${xPct}%`;
markerRef.current.style.top = `${yPct}%`;
}, []);
return <div ref={markerRef} className="marker" />;
}
-
useEffect(..., [])… マウント後一度だけ実行(Rectが読めるタイミング) -
style.left/top = %… そのままCSSに反映
よくあるハマりどころ(優先度順)
-
区間長が0(a===b, width/height=0)
→ レイアウト未確定。onMounted + nextTick(Vue)/useEffect(React)で描画後に測る
→ 安全版unlerp01/toPercentInRectを使う -
display:none の要素を測っている
→ Rectは常に0×0。まず表示状態で測る(visibility:hiddenや画面外配置も可) -
transform の影響
→getBoundingClientRect()は見た目(変形後)のサイズ
→ レイアウト寸法ならoffsetWidth/Heightと使い分け -
%が 0〜100 をはみ出す
→ それでOKな演出もあるが、通常はclampで切り詰める -
スクロール・ネストスクロール
→ 親子とも Rect(ビューポート基準)で差分を取れば相殺される
確認すべきファイル/コードと意図
-
HTML/テンプレ
-
.containerは1つ・サイズが確定(position: relative; overflow: hidden;推奨) -
.childは正しいセレクタで取得できる
-
-
CSS
-
.marker { position: absolute; }(%の基準を親にしたい) - レスポンシブ時も親の width/height が0にならないように
-
-
TS/JS
- 測定はマウント後(Vue:
onMounted+nextTick, React:useEffect) - リサイズ時は 再測定(
ResizeObserverorwindow.onresize)
- 測定はマウント後(Vue:
参考(公式ドキュメント)
- MDN: Element.getBoundingClientRect()(ビューポート基準の矩形)
https://developer.mozilla.org/ja/docs/Web/API/Element/getBoundingClientRect - MDN: DOMRect
https://developer.mozilla.org/ja/docs/Web/API/DOMRect - MDN: 値と単位(%)
https://developer.mozilla.org/ja/docs/Learn/CSS/Building_blocks/Values_and_units#percentages - Vue 公式:クラスとスタイルのバインディング
https://ja.vuejs.org/guide/essentials/class-and-style.html - React 公式:useEffect
https://react.dev/reference/react/useEffect
まとめ
- unlerp = (x - a) / (b - a) で「今どれくらい進んだか(0〜1)」が取れる
-
px → % の変換にぴったり。
getBoundingClientRect()とセットで使うとスクロールにも強い
Discussion