補間(Interpolation)
2 つの値の中間を求める計算。トゥイーンの本体。 数値・座標・カラー・配列・角度それぞれの補間方法。
線形補間(lerp)
最も基本。t が 0〜1 の範囲で、a と b の間を直線的に進む。
function lerp(a, b, t) {
return a + (b - a) * t
}
lerp(0, 100, 0) // 0
lerp(0, 100, 0.5) // 50
lerp(0, 100, 1) // 100
lerp(0, 100, 0.3) // 30
逆 lerp(inverseLerp)
値からその位置(0〜1)を求める:
function invLerp(a, b, v) {
return (v - a) / (b - a)
}
invLerp(0, 100, 30) // 0.3
remap(再マッピング)
ある範囲を別の範囲に変換。「マウス X 座標を回転角度に」「スクロール量を opacity に」などで多用。
function remap(v, inMin, inMax, outMin, outMax) {
return outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin)
}
remap(50, 0, 100, 0, 1) // 0.5
remap(800, 0, 1000, -10, 10) // 6
remap(scrollY, 0, 500, 0, 1) // よくある使い方
clamp
範囲を制限する。lerp / remap と組み合わせると範囲外で暴れない。
function clamp(v, min = 0, max = 1) {
return Math.min(Math.max(v, min), max)
}
const t = clamp((now - start) / duration) // 0〜1 に確実に
smoothstep / smootherstep
境界が滑らかな t(イージングの一種)。GLSL 由来だが JS でも便利。
function smoothstep(edge0, edge1, x) {
const t = clamp((x - edge0) / (edge1 - edge0))
return t * t * (3 - 2 * t) // 3t² - 2t³
}
function smootherstep(edge0, edge1, x) {
const t = clamp((x - edge0) / (edge1 - edge0))
return t * t * t * (t * (t * 6 - 15) + 10) // 6t⁵ - 15t⁴ + 10t³
}
ベクトル / オブジェクトの補間
複数のフィールドを各成分ごとに lerp する。
function lerpObject(a, b, t) {
const out = {}
for (const key in a) {
if (typeof a[key] === "number") {
out[key] = a[key] + (b[key] - a[key]) * t
} else {
out[key] = a[key]
}
}
return out
}
const start = { x: 0, y: 0, opacity: 0 }
const end = { x: 200, y: 100, opacity: 1 }
lerpObject(start, end, 0.5)
// { x: 100, y: 50, opacity: 0.5 }
3D ベクトル
function lerpVec3(a, b, t) {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
]
}
// Three.js でも標準で使える
vec.lerp(target, t)
vec.lerpVectors(start, end, t)
角度の補間(最短経路)
単純 lerp だと「359° → 1°」が「-358°」を経由してしまう。±180° 以内を選ぶ。
function lerpAngle(a, b, t) {
let diff = ((b - a + 540) % 360) - 180 // -180〜180
return a + diff * t
}
lerpAngle(350, 10, 0.5) // 360 (= 0)
// 単純 lerp だと 180 になってしまう
ラジアン版
const TAU = Math.PI * 2
function lerpRad(a, b, t) {
let diff = ((b - a + Math.PI * 3) % TAU) - Math.PI
return a + diff * t
}
カラー補間
RGB 補間(最も単純)
function lerpRgb(a, b, t) {
return {
r: Math.round(a.r + (b.r - a.r) * t),
g: Math.round(a.g + (b.g - a.g) * t),
b: Math.round(a.b + (b.b - a.b) * t),
}
}
lerpRgb({r: 255, g: 0, b: 0}, {r: 0, g: 0, b: 255}, 0.5)
// { r: 128, g: 0, b: 128 } 暗紫色(中間が灰色っぽくなる問題)
RGB 直接補間は中間が暗くなるのが弱点。原因はガンマ補正されていない sRGB 空間で線形補間しているから。
線形 RGB 補間(より正しい)
// sRGB → linear
function srgbToLinear(c) {
c /= 255
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
}
function linearToSrgb(c) {
const v = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1/2.4) - 0.055
return Math.round(Math.min(Math.max(v, 0), 1) * 255)
}
function lerpRgbLinear(a, b, t) {
const lerpCh = (ca, cb) =>
linearToSrgb(srgbToLinear(ca) + (srgbToLinear(cb) - srgbToLinear(ca)) * t)
return { r: lerpCh(a.r, b.r), g: lerpCh(a.g, b.g), b: lerpCh(a.b, b.b) }
}
HSL 補間(色相を回す)
function lerpHsl(a, b, t) {
return {
h: lerpAngle(a.h, b.h, t), // 色相は環状なので最短経路
s: a.s + (b.s - a.s) * t,
l: a.l + (b.l - a.l) * t,
}
}
OKLCH / OKLAB(最も知覚的に均一)
2024 年以降の Web で標準化された知覚均一な色空間。CSS の color-mix(in oklch, ...) もこれ。
中間色がきれい。
// CSS Color Module Level 4 を使えるなら
element.style.color = `color-mix(in oklch, red ${(1-t)*100}%, blue)`
カラー文字列のパース
function parseHex(hex) {
hex = hex.replace("#", "")
if (hex.length === 3) hex = hex.split("").map(c => c + c).join("")
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
}
}
function rgbString({ r, g, b }) {
return `rgb(${r}, ${g}, ${b})`
}
const start = parseHex("#ff0000")
const end = parseHex("#0000ff")
const mid = lerpRgb(start, end, 0.5)
element.style.color = rgbString(mid)
配列の補間
function lerpArray(a, b, t) {
return a.map((v, i) => v + (b[i] - v) * t)
}
lerpArray([0, 0, 0], [10, 20, 30], 0.5)
// [5, 10, 15]
クォータニオン(3D 回転)の補間
3D 回転はクォータニオン (Quaternion) で表現するのが標準。線形補間ではなく slerp(球面線形補間)を使う。
Three.js の Quaternion.slerp がそれ。
// Three.js で
quat.slerpQuaternions(qStart, qEnd, t)
// 短く
quat.slerp(qTarget, t)
damp(フレームレート独立な lerp)
毎フレーム同じ t で lerp するとFPS で速度が変わる。
指数減衰の式にするとフレームレート独立になる。
// dt: 経過時間(秒), smoothing: 0.001〜0.1(0 で即追従、大きいほどゆっくり)
function damp(current, target, smoothing, dt) {
return target + (current - target) * Math.exp(-dt / smoothing)
}
// 使い方
let x = 0
function loop(now) {
const dt = (now - prev) / 1000
x = damp(x, mouseX, 0.1, dt) // mouseX に向かって減衰追従
...
}
damp の挙動
damp は終わりがない(asymptotic)。100% は到達しない。マウス追従などの連続的な追従に向く。 トゥイーンとは別ジャンル。
ステップ補間
パラパラ漫画的な、補間しない離散値。
function step(values, t) {
const i = Math.min(Math.floor(t * values.length), values.length - 1)
return values[i]
}
step(["a", "b", "c", "d"], 0.5) // "c"
カトマルロムスプライン(4 点を通る滑らか曲線)
複数のキーフレームを滑らかにつなぐのに使う。3D パスやカメラワークで便利。
function catmullRom(p0, p1, p2, p3, t) {
const t2 = t * t
const t3 = t2 * t
return 0.5 * (
(2 * p1) +
(-p0 + p2) * t +
(2*p0 - 5*p1 + 4*p2 - p3) * t2 +
(-p0 + 3*p1 - 3*p2 + p3) * t3
)
}
// 配列の中で適切な 4 点を選ぶ
function spline(points, t) {
const seg = (points.length - 1) * t
const i = Math.floor(seg)
const localT = seg - i
const p0 = points[Math.max(i - 1, 0)]
const p1 = points[i]
const p2 = points[Math.min(i + 1, points.length - 1)]
const p3 = points[Math.min(i + 2, points.length - 1)]
return catmullRom(p0, p1, p2, p3, localT)
}
Three.js には CatmullRomCurve3 として完成版がある。
ベジェ曲線
2 次ベジェ(3 点)
function quadBezier(p0, p1, p2, t) {
const u = 1 - t
return u*u*p0 + 2*u*t*p1 + t*t*p2
}
3 次ベジェ(4 点、CSS の cubic-bezier と同じ)
function cubicBezier(p0, p1, p2, p3, t) {
const u = 1 - t
return u*u*u*p0 + 3*u*u*t*p1 + 3*u*t*t*p2 + t*t*t*p3
}
CSS の cubic-bezier(.25, .1, .25, 1) はこれを 1 次元のイージング関数として使ったもの。
SVG パスに沿わせる
const path = document.querySelector("path")
const total = path.getTotalLength()
function loop(now) {
const t = ((now / 1000) % 5) / 5 // 5 秒で 1 周
const pt = path.getPointAtLength(t * total)
element.style.transform = `translate(${pt.x}px, ${pt.y}px)`
requestAnimationFrame(loop)
}
Bezier ベースのイージング
CSS の cubic-bezier(x1, y1, x2, y2) を JS で再現する場合、(0,0) と (1,1) を端点にした
2 次元のベジェから x が t になる位置を逆算 → その位置の y を返す。
function cubicBezierEase(x1, y1, x2, y2) {
// ニュートン法で x → t を解いて y を返す
return function(x) {
let t = x
for (let i = 0; i < 8; i++) {
const ct = cubicBezier(0, x1, x2, 1, t) - x
const dt = 3*(1-t)*(1-t)*x1 + 6*(1-t)*t*(x2-x1) + 3*t*t*(1-x2)
if (Math.abs(dt) < 1e-6) break
t -= ct / dt
}
return cubicBezier(0, y1, y2, 1, t)
}
}
const easeInOut = cubicBezierEase(0.42, 0, 0.58, 1)
easeInOut(0.5)
整数の補間(離散値)
スコアの増加など、整数で表示したいとき:
function lerpInt(a, b, t) {
return Math.round(a + (b - a) * t)
}
文字列の補間
単純な文字列はできないが、「タイプライター効果」のような文字数を増やすことはできる:
function lerpString(text, t) {
const len = Math.round(text.length * t)
return text.slice(0, len)
}
使い分けまとめ
| 状況 | 関数 |
|---|---|
| 数値の中間 | lerp |
| マウス追従 | damp |
| 角度 | lerpAngle |
| RGB の高品質補間 | lerpRgbLinear / OKLCH |
| 3D 回転 | Quaternion.slerp |
| 多点を滑らか | catmullRom |
| イージング曲線 | cubicBezierEase |
lerp(a, b, t) = a + (b - a) * t。これを起点に、次元 / 色空間 / 球面 / 曲線へ
展開していくだけ。