75 lines
2.2 KiB
JavaScript
75 lines
2.2 KiB
JavaScript
/**
|
||
* One-Euro Filter(速度自适应低通滤波器)
|
||
* 参考:Casiez et al., "1€ Filter: A Simple Speed-based Low-pass Filter for Noisy Input in Interactive Systems", CHI 2012
|
||
* https://cristal.univ-lille.fr/~casiez/1euro/
|
||
*
|
||
* 用法:每个轴一个实例
|
||
* const f = createOneEuroFilter({ mincutoff: 0.2, beta: 0.05, dcutoff: 1.0 })
|
||
* const out = f.filter(x, performance.now())
|
||
* f.reset()
|
||
*
|
||
* @param {object} [opts]
|
||
* @param {number} [opts.mincutoff=0.2] - Hz,静止截止频率
|
||
* @param {number} [opts.beta=0.05] - 速度系数(越大快速运动越跟手)
|
||
* @param {number} [opts.dcutoff=1.0] - Hz,速度估计的截止频率
|
||
*/
|
||
export function createOneEuroFilter({ mincutoff = 0.2, beta = 0.05, dcutoff = 1.0 } = {}) {
|
||
let xPrev = null // 上次 FILTERED 输出(与 Casiez 论文一致)
|
||
let dxPrev = 0 // 平滑后的速度
|
||
let tPrev = null // 上次时间戳
|
||
|
||
function smoothingFactor(cutoff, dt) {
|
||
const r = 2 * Math.PI * cutoff * dt
|
||
return r / (r + 1)
|
||
}
|
||
|
||
function filter(x, t) {
|
||
// 首帧:直接接受,建立基线
|
||
if (xPrev == null || tPrev == null) {
|
||
xPrev = x
|
||
dxPrev = 0
|
||
tPrev = t
|
||
return x
|
||
}
|
||
|
||
const dt = Math.max((t - tPrev) / 1000, 1e-6) // ms → s
|
||
|
||
// 1. 速度差分(用 filtered xPrev,与 Casiez 一致)
|
||
const dx = (x - xPrev) / dt
|
||
|
||
// 2. 平滑速度
|
||
const aD = smoothingFactor(dcutoff, dt)
|
||
const edx = aD * dx + (1 - aD) * dxPrev
|
||
|
||
// 3. 自适应截止频率
|
||
const cutoff = mincutoff + beta * Math.abs(edx)
|
||
|
||
// 4. 平滑位置
|
||
const a = smoothingFactor(cutoff, dt)
|
||
const result = a * x + (1 - a) * xPrev
|
||
|
||
xPrev = result
|
||
dxPrev = edx
|
||
tPrev = t
|
||
return result
|
||
}
|
||
|
||
function reset() {
|
||
xPrev = null
|
||
dxPrev = 0
|
||
tPrev = null
|
||
}
|
||
|
||
return { filter, reset }
|
||
}
|
||
|
||
/**
|
||
* 取单调递增的高精度时间戳(毫秒)。
|
||
* 优先 performance.now()(sub-ms 精度),回退 Date.now()(WebView 上 ~15ms 精度)。
|
||
*/
|
||
export function nowMs() {
|
||
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
||
return performance.now()
|
||
}
|
||
return Date.now()
|
||
} |