769 lines
23 KiB
JavaScript
769 lines
23 KiB
JavaScript
/**
|
||
* 光栅卡工作室倾斜驱动:App 优先 imengyu-UniAndroidGyro(DCloud 插件 id=6237),否则加速度计。
|
||
*
|
||
* 与插件约定对齐(官方说明):
|
||
* - 模块:`imengyu-UniAndroidGyro-GyroModule`,`uni.requireNativePlugin`。
|
||
* - `startGyro`:首包回调仅 success/errMsg/notSupport(**无 x/y/z 角度属正常**);持续数据需 **`getGyroValue` 轮询**(插件官方示例);勿把首包当「无参数」故障。
|
||
* - `startGyroWithCallback`:部分自定义基座 + HX SDK 组合下长期收不到角度帧;本实现以 **startGyro + 轮询** 为主,WithCallback 为兜底。
|
||
* - 轮询:插件若同时提供 `getGyroValue` 与 `getGyroValueSync`,**优先异步**(与官方示例一致);部分环境下 Sync 长期无 x/y/z,易触发看门狗回退。
|
||
* - 再次开启前须 `getGyroStarted`;若已在监听则先 `stopGyro`(不可重复开启)。
|
||
* - 页面进入后立即监听前宜延时再 start(官方示例约 100ms)。
|
||
* - 插件返回的 x/y/z 为**当前各轴角度(度)**,与「进入光栅卡时采样的基准角」做差得到相对姿态;离散档位应使用**与轴向切换无关**的标量(如 max(|Δx|,|Δy|,|Δz|)),避免「谁幅度大跟谁」在临界区来回跳轴导致画面乱切。
|
||
* - 原生陀螺看门狗:按「**连续无有效角度帧**」计时(每来一帧即重置),避免固定短超时与晚首包 / `getGyroStarted`→`stopGyro`→`kick` 慢链冲突;超时仍无帧再回退加速度计。
|
||
*
|
||
* @see https://ext.dcloud.net.cn/plugin?id=6237
|
||
*/
|
||
|
||
const NATIVE_PLUGIN_ID = 'imengyu-UniAndroidGyro-GyroModule'
|
||
|
||
/** iOS 等环境下布尔可能为字符串 `'true'` */
|
||
function isTruthyFlag(v) {
|
||
return v === true || v === 'true'
|
||
}
|
||
|
||
function hasAnglePayload(res) {
|
||
if (!res || typeof res !== 'object') return false
|
||
/** 插件首包常无 x/y/z;有数值(含 0)才视为角度帧 */
|
||
return [res.x, res.y, res.z].some((v) => Number.isFinite(Number(v)))
|
||
}
|
||
|
||
/** 无角度包时:仅明确失败才放弃原生;success 缺省不等同于失败(部分 ROM 首包字段不全) */
|
||
function isExplicitGyroHandshakeFailure(res) {
|
||
if (!res || typeof res !== 'object') return false
|
||
if (isTruthyFlag(res.notSupport)) return true
|
||
if (res.success === false || res.success === 'false') return true
|
||
if (res.success === '') return true
|
||
return false
|
||
}
|
||
|
||
// #ifdef APP-PLUS
|
||
function tryRequireImengyuGyro() {
|
||
try {
|
||
if (typeof uni === 'undefined' || typeof uni.requireNativePlugin !== 'function') return null
|
||
const mod = uni.requireNativePlugin(NATIVE_PLUGIN_ID)
|
||
const okPoll =
|
||
mod &&
|
||
typeof mod.startGyro === 'function' &&
|
||
typeof mod.stopGyro === 'function' &&
|
||
(typeof mod.getGyroValue === 'function' || typeof mod.getGyroValueSync === 'function')
|
||
const okCb =
|
||
mod &&
|
||
typeof mod.startGyroWithCallback === 'function' &&
|
||
typeof mod.stopGyro === 'function'
|
||
if (okPoll || okCb) {
|
||
return mod
|
||
}
|
||
} catch (e) {
|
||
console.warn('[useLenticularStudioTilt] requireNativePlugin failed', e)
|
||
}
|
||
return null
|
||
}
|
||
// #endif
|
||
// #ifndef APP-PLUS
|
||
function tryRequireImengyuGyro() {
|
||
return null
|
||
}
|
||
// #endif
|
||
|
||
/** 基线帧数 */
|
||
const ACCEL_BASELINE_FRAMES = 10
|
||
const INSTANT_FULL_RAD = 0.32
|
||
const MAX_STEP_RAD = 0.48
|
||
const STEP_GAIN = 2.15
|
||
const SIN_PHASE_SCALE = 1.18
|
||
const BLEND_INSTANT = 0.38
|
||
|
||
/** 原生插件:x/y/z 均为角度(度);进入页面后先采若干帧算**基准角**(与官方「先 start 再轮询 getGyroValue」一致) */
|
||
const NATIVE_BASELINE_FRAMES = 12
|
||
const NATIVE_FULL_DEG = 10
|
||
|
||
/** 加速度计直连 + 离散档位:进入后多帧圆均值定「水平」基准,避免首帧抖动 */
|
||
const STUDIO_ACCEL_BASELINE_FRAMES = 12
|
||
|
||
/** 连续无有效 x/y/z 角度帧则回退加速度计(ms);覆盖 getGyroStarted→stopGyro→kick 慢链与晚首包 */
|
||
const NATIVE_GYRO_STALL_FALLBACK_MS = 14000
|
||
|
||
function deltaDeg(value, base) {
|
||
const v = Number(value)
|
||
const b = Number(base)
|
||
if (!Number.isFinite(v) || !Number.isFinite(b)) return 0
|
||
let d = v - b
|
||
while (d > 180) d -= 360
|
||
while (d < -180) d += 360
|
||
return d
|
||
}
|
||
|
||
/** 取与基线差最大的轴,避免握持方向不同导致「陀螺仪没反应」 */
|
||
function pickDominantTiltDelta(dx, dy, dz) {
|
||
const ax = Math.abs(dx)
|
||
const ay = Math.abs(dy)
|
||
const az = Math.abs(dz)
|
||
if (ax >= ay && ax >= az) return dx
|
||
if (ay >= az) return dy
|
||
return dz
|
||
}
|
||
|
||
/** 相对基准的最大欧拉偏差(度),左右/多轴合成时仍单调,且不会在临界区因「换轴」突变符号 */
|
||
function maxAbsTiltDeltaDeg(dx, dy, dz) {
|
||
return Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz))
|
||
}
|
||
|
||
/**
|
||
* 根据进入后采集的样本,选「抖动范围最大」的轴作为连续跟手的倾角轴(基线期手持微动时仍稳定)。
|
||
* @param {{ x: number, y: number, z: number }[]} samples
|
||
* @returns {0|1|2}
|
||
*/
|
||
function inferPrimaryTiltAxisFromSamples(samples) {
|
||
if (!samples || samples.length < 2) return 1
|
||
let bestAxis = 1
|
||
let bestSpread = -1
|
||
for (let axis = 0; axis < 3; axis++) {
|
||
const vals = samples.map((s) => [s.x, s.y, s.z][axis])
|
||
const mn = Math.min(...vals)
|
||
const mx = Math.max(...vals)
|
||
const spread = mx - mn
|
||
if (spread > bestSpread) {
|
||
bestSpread = spread
|
||
bestAxis = axis
|
||
}
|
||
}
|
||
/* 几乎完全静止:竖屏常见左右倾角在 y */
|
||
if (bestSpread < 0.35) return 1
|
||
return bestAxis
|
||
}
|
||
|
||
function circularMeanRad(rads) {
|
||
if (!rads.length) return 0
|
||
let sx = 0
|
||
let sy = 0
|
||
for (const r of rads) {
|
||
sx += Math.sin(r)
|
||
sy += Math.cos(r)
|
||
}
|
||
return Math.atan2(sx / rads.length, sy / rads.length)
|
||
}
|
||
|
||
function majorityPlane(votes) {
|
||
const cnt = { xz: 0, xy: 0, yz: 0 }
|
||
for (const v of votes) cnt[v] = (cnt[v] || 0) + 1
|
||
if (cnt.xz >= cnt.xy && cnt.xz >= cnt.yz) return 'xz'
|
||
if (cnt.xy >= cnt.yz) return 'xy'
|
||
return 'yz'
|
||
}
|
||
|
||
function detectGravityPlane(nx, ny, nz) {
|
||
const ax = Math.abs(nx)
|
||
const ay = Math.abs(ny)
|
||
const az = Math.abs(nz)
|
||
if (ay >= ax && ay >= az) return 'xz'
|
||
if (az >= ax && az >= ay) return 'xy'
|
||
return 'yz'
|
||
}
|
||
|
||
function uvForPlane(nx, ny, nz, mode) {
|
||
if (mode === 'xz') return { u: nx, v: nz }
|
||
if (mode === 'xy') return { u: nx, v: ny }
|
||
return { u: ny, v: nz }
|
||
}
|
||
|
||
/**
|
||
* @param {object} opts
|
||
* @param {(x: number, y: number) => void} opts.simulate
|
||
* @param {(tiltMagDeg: number) => void} [opts.simulateFromSignedDegrees] 若提供:用相对进入时基准的**倾角标量(度)**驱动预览(由页面做离散档位等);原生/加速度计侧会喂入非负幅度为主
|
||
* @param {import('vue').Ref<string>} opts.gyroSourceLabel
|
||
* @param {boolean} [opts.useStudioAccelDirect] 光栅工作室:加速度计用重力投影直连(免原 warmup)
|
||
* @param {() => void} [opts.onTiltDriverFallback] 从原生陀螺失败/超时回退到加速度计时调用(用于重置离散档位等 UI 状态)
|
||
*/
|
||
export function useLenticularStudioTilt(opts) {
|
||
const {
|
||
simulate,
|
||
simulateFromSignedDegrees,
|
||
gyroSourceLabel,
|
||
useStudioAccelDirect = false,
|
||
onTiltDriverFallback,
|
||
} = opts
|
||
|
||
let mode = /** @type {'native'|'accel'} */ ('accel')
|
||
let gyroModule = null
|
||
let nativeStartTimer = null
|
||
let nativeWatchdogTimer = null
|
||
let gyroCbGuardTimer = null
|
||
let nativePollTimer = null
|
||
/** @type {{ x: number, y: number, z: number }[]} */
|
||
let nativeTrSamples = []
|
||
let nativeBase = { x: 0, y: 0, z: 0 }
|
||
let nativeBaselineReady = false
|
||
let nativeSmoothed = 0
|
||
/** 连续模式下与基线样本推断的主倾角轴(0=x,1=y,2=z),避免每帧在 x/y 间抢主导 */
|
||
let nativeLockedAxisIdx = 1
|
||
/** 递增以丢弃 stop 之后的原生回调 / 延时任务 */
|
||
let tiltGen = 0
|
||
/** 原生:每次收到角度帧时重置该计时器;超时仍无帧则回退加速度计 */
|
||
let scheduleNativeGyroStallFallback = /** @type {null | (() => void)} */ (null)
|
||
|
||
/** 加速度计直连:多帧采样缓冲,填满后取圆均值为基准角 */
|
||
let studioAccelBaselineRads = []
|
||
/** 加速度计直连模式下「校零」后的基准(弧度),仅与 simulateFromSignedDegrees 联用 */
|
||
let studioAccelBaseRad = null
|
||
|
||
let accelHandler = null
|
||
let accelSmoothed = 0
|
||
let rollAccum = 0
|
||
let prevCu = null
|
||
let prevCv = null
|
||
|
||
let tiltCal = {
|
||
lockedMode: null,
|
||
planeVotes: [],
|
||
sumU: 0,
|
||
sumV: 0,
|
||
count: 0,
|
||
ready: false,
|
||
bu: 1,
|
||
bv: 0,
|
||
}
|
||
|
||
function resetAccelCalibration() {
|
||
tiltCal = {
|
||
lockedMode: null,
|
||
planeVotes: [],
|
||
sumU: 0,
|
||
sumV: 0,
|
||
count: 0,
|
||
ready: false,
|
||
bu: 1,
|
||
bv: 0,
|
||
}
|
||
}
|
||
|
||
function resetNativeBaseline() {
|
||
nativeTrSamples = []
|
||
nativeBase = { x: 0, y: 0, z: 0 }
|
||
nativeBaselineReady = false
|
||
nativeSmoothed = 0
|
||
nativeLockedAxisIdx = 1
|
||
}
|
||
|
||
function resetStudioAccelBaseline() {
|
||
studioAccelBaseRad = null
|
||
studioAccelBaselineRads = []
|
||
}
|
||
|
||
function accelToRelativeTilt01(ax, ay, az) {
|
||
const gMag = Math.hypot(ax, ay, az)
|
||
if (gMag < 0.18) return null
|
||
const nx = ax / gMag
|
||
const ny = ay / gMag
|
||
const nz = az / gMag
|
||
|
||
if (!tiltCal.ready) {
|
||
const m = detectGravityPlane(nx, ny, nz)
|
||
if (tiltCal.planeVotes.length < 5) tiltCal.planeVotes.push(m)
|
||
if (tiltCal.planeVotes.length >= 5 && tiltCal.lockedMode == null) {
|
||
tiltCal.lockedMode = majorityPlane(tiltCal.planeVotes)
|
||
tiltCal.sumU = 0
|
||
tiltCal.sumV = 0
|
||
tiltCal.count = 0
|
||
}
|
||
if (tiltCal.lockedMode == null) return { warmup: true }
|
||
const { u, v } = uvForPlane(nx, ny, nz, tiltCal.lockedMode)
|
||
const h = Math.hypot(u, v)
|
||
if (h < 0.035) return { warmup: true }
|
||
tiltCal.sumU += u / h
|
||
tiltCal.sumV += v / h
|
||
tiltCal.count += 1
|
||
if (tiltCal.count >= ACCEL_BASELINE_FRAMES) {
|
||
const bh = Math.hypot(tiltCal.sumU, tiltCal.sumV) || 1
|
||
tiltCal.bu = tiltCal.sumU / bh
|
||
tiltCal.bv = tiltCal.sumV / bh
|
||
tiltCal.ready = true
|
||
}
|
||
return { warmup: true }
|
||
}
|
||
|
||
const { u, v } = uvForPlane(nx, ny, nz, tiltCal.lockedMode)
|
||
const h = Math.hypot(u, v)
|
||
if (h < 0.028) return null
|
||
const cu = u / h
|
||
const cv = v / h
|
||
const sinD = cu * tiltCal.bv - cv * tiltCal.bu
|
||
const cosD = cu * tiltCal.bu + cv * tiltCal.bv
|
||
const deltaRad = Math.atan2(sinD, cosD)
|
||
return { warmup: false, cu, cv, deltaRad }
|
||
}
|
||
|
||
function stopNative() {
|
||
tiltGen++
|
||
if (nativeWatchdogTimer != null) {
|
||
try {
|
||
clearTimeout(nativeWatchdogTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
nativeWatchdogTimer = null
|
||
}
|
||
if (gyroCbGuardTimer != null) {
|
||
try {
|
||
clearTimeout(gyroCbGuardTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
gyroCbGuardTimer = null
|
||
}
|
||
if (nativePollTimer != null) {
|
||
try {
|
||
clearInterval(nativePollTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
nativePollTimer = null
|
||
}
|
||
if (nativeStartTimer != null) {
|
||
try {
|
||
clearTimeout(nativeStartTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
nativeStartTimer = null
|
||
}
|
||
if (gyroModule && typeof gyroModule.stopGyro === 'function') {
|
||
try {
|
||
gyroModule.stopGyro(() => {})
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
}
|
||
gyroModule = null
|
||
mode = 'accel'
|
||
scheduleNativeGyroStallFallback = null
|
||
}
|
||
|
||
function stopAccel() {
|
||
try {
|
||
if (accelHandler && typeof uni.offAccelerometerChange === 'function') {
|
||
uni.offAccelerometerChange(accelHandler)
|
||
}
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
accelHandler = null
|
||
try {
|
||
uni.stopAccelerometer({})
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
}
|
||
|
||
function startAccelInternal(options = {}) {
|
||
const fromNativeFallback = options.fromNativeFallback === true
|
||
mode = 'accel'
|
||
stopAccel()
|
||
if (fromNativeFallback && typeof onTiltDriverFallback === 'function') {
|
||
try {
|
||
onTiltDriverFallback()
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
}
|
||
resetAccelCalibration()
|
||
accelSmoothed = 0
|
||
rollAccum = 0
|
||
prevCu = null
|
||
prevCv = null
|
||
|
||
if (useStudioAccelDirect) {
|
||
if (typeof simulateFromSignedDegrees === 'function') {
|
||
resetStudioAccelBaseline()
|
||
}
|
||
accelHandler = (res) => {
|
||
const ax = Number(res.x) || 0
|
||
const ay = Number(res.y) || 0
|
||
const az = Number(res.z) || 0
|
||
const gMag = Math.hypot(ax, ay, az)
|
||
if (gMag < 0.12) return
|
||
/* 竖屏常见握持:左右倾斜主要反映为 ax/az 与重力的关系(免 warmup,避免长期无输出) */
|
||
if (typeof simulateFromSignedDegrees === 'function') {
|
||
const tiltRad = Math.atan2(-ax, az)
|
||
if (studioAccelBaseRad == null) {
|
||
studioAccelBaselineRads.push(tiltRad)
|
||
if (studioAccelBaselineRads.length < STUDIO_ACCEL_BASELINE_FRAMES) {
|
||
simulate(0, 0)
|
||
return
|
||
}
|
||
studioAccelBaseRad = circularMeanRad(studioAccelBaselineRads)
|
||
studioAccelBaselineRads = []
|
||
}
|
||
let delta = tiltRad - studioAccelBaseRad
|
||
while (delta > Math.PI) delta -= 2 * Math.PI
|
||
while (delta < -Math.PI) delta += 2 * Math.PI
|
||
const signedDeg = delta * (180 / Math.PI)
|
||
simulateFromSignedDegrees(Math.abs(signedDeg))
|
||
} else {
|
||
const tiltRaw = Math.atan2(-ax, az) / (Math.PI / 5)
|
||
const tilt01 = Math.max(-1, Math.min(1, tiltRaw))
|
||
accelSmoothed += (tilt01 - accelSmoothed) * 0.58
|
||
simulate(Math.max(-1, Math.min(1, accelSmoothed)), 0)
|
||
}
|
||
gyroSourceLabel.value = 'accelerometer'
|
||
}
|
||
} else {
|
||
accelHandler = (res) => {
|
||
const ax = Number(res.x) || 0
|
||
const ay = Number(res.y) || 0
|
||
const az = Number(res.z) || 0
|
||
const out = accelToRelativeTilt01(ax, ay, az)
|
||
if (out == null) return
|
||
if (out.warmup) return
|
||
const { cu, cv, deltaRad } = out
|
||
if (prevCu != null && prevCv != null) {
|
||
let step = Math.atan2(cu * prevCv - cv * prevCu, cu * prevCu + cv * prevCv)
|
||
if (step > Math.PI * 0.92) step -= 2 * Math.PI
|
||
if (step < -Math.PI * 0.92) step += 2 * Math.PI
|
||
step = Math.max(-MAX_STEP_RAD, Math.min(MAX_STEP_RAD, step))
|
||
rollAccum += step * STEP_GAIN
|
||
}
|
||
prevCu = cu
|
||
prevCv = cv
|
||
|
||
const instant = Math.max(-1, Math.min(1, deltaRad / INSTANT_FULL_RAD))
|
||
const cyclic = Math.sin(rollAccum * SIN_PHASE_SCALE)
|
||
const target = BLEND_INSTANT * instant + (1 - BLEND_INSTANT) * cyclic
|
||
|
||
accelSmoothed += (target - accelSmoothed) * 0.74
|
||
simulate(Math.max(-1, Math.min(1, accelSmoothed)), 0)
|
||
gyroSourceLabel.value = 'accelerometer'
|
||
}
|
||
}
|
||
try {
|
||
uni.onAccelerometerChange(accelHandler)
|
||
const applyStart = (interval) => {
|
||
uni.startAccelerometer({
|
||
interval,
|
||
success: () => {
|
||
gyroSourceLabel.value = 'accelerometer'
|
||
},
|
||
fail: () => {
|
||
if (interval === 'game') {
|
||
applyStart('normal')
|
||
return
|
||
}
|
||
gyroSourceLabel.value = 'simulation'
|
||
},
|
||
})
|
||
}
|
||
applyStart('game')
|
||
} catch (e) {
|
||
gyroSourceLabel.value = 'simulation'
|
||
}
|
||
}
|
||
|
||
function onNativeAngleFrame(x, y, z) {
|
||
const vx = Number(x)
|
||
const vy = Number(y)
|
||
const vz = Number(z)
|
||
if (![vx, vy, vz].some(Number.isFinite)) return
|
||
/* 有角度帧即视为陀螺仪在工作,含基线采集期,避免看门狗误判「从未产出」而中途切加速度计 */
|
||
gyroSourceLabel.value = 'gyroscope'
|
||
|
||
if (!nativeBaselineReady) {
|
||
if ([vx, vy, vz].every(Number.isFinite)) {
|
||
nativeTrSamples.push({ x: vx, y: vy, z: vz })
|
||
}
|
||
if (nativeTrSamples.length >= NATIVE_BASELINE_FRAMES) {
|
||
const n = nativeTrSamples.length
|
||
nativeBase.x = nativeTrSamples.reduce((a, b) => a + b.x, 0) / n
|
||
nativeBase.y = nativeTrSamples.reduce((a, b) => a + b.y, 0) / n
|
||
nativeBase.z = nativeTrSamples.reduce((a, b) => a + b.z, 0) / n
|
||
nativeLockedAxisIdx = inferPrimaryTiltAxisFromSamples(nativeTrSamples)
|
||
nativeBaselineReady = true
|
||
}
|
||
simulate(0, 0)
|
||
if (typeof scheduleNativeGyroStallFallback === 'function') {
|
||
scheduleNativeGyroStallFallback()
|
||
}
|
||
return
|
||
}
|
||
|
||
const dx = deltaDeg(vx, nativeBase.x)
|
||
const dy = deltaDeg(vy, nativeBase.y)
|
||
const dz = deltaDeg(vz, nativeBase.z)
|
||
if (typeof simulateFromSignedDegrees === 'function') {
|
||
simulateFromSignedDegrees(maxAbsTiltDeltaDeg(dx, dy, dz))
|
||
} else {
|
||
const axisDeltas = [dx, dy, dz]
|
||
const chosen = axisDeltas[nativeLockedAxisIdx] ?? pickDominantTiltDelta(dx, dy, dz)
|
||
const tilt01 = Math.max(-1, Math.min(1, chosen / NATIVE_FULL_DEG))
|
||
/* 跟手:过低显钝;过高易抖。插件已做角度融合,此处略轻低通即可 */
|
||
nativeSmoothed += (tilt01 - nativeSmoothed) * 0.86
|
||
simulate(Math.max(-1, Math.min(1, nativeSmoothed)), 0)
|
||
}
|
||
if (typeof scheduleNativeGyroStallFallback === 'function') {
|
||
scheduleNativeGyroStallFallback()
|
||
}
|
||
}
|
||
|
||
function startNativeInternal() {
|
||
stopNative()
|
||
stopAccel()
|
||
resetNativeBaseline()
|
||
// #ifdef APP-PLUS
|
||
gyroModule = tryRequireImengyuGyro()
|
||
if (!gyroModule) {
|
||
mode = 'accel'
|
||
startAccelInternal()
|
||
return
|
||
}
|
||
mode = 'native'
|
||
const myGen = tiltGen
|
||
/** 与官方示例一致:normal / ui / game / fastest,game≈50Hz */
|
||
const startOpts = { interval: 'game' }
|
||
|
||
scheduleNativeGyroStallFallback = () => {
|
||
if (nativeWatchdogTimer != null) {
|
||
try {
|
||
clearTimeout(nativeWatchdogTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
nativeWatchdogTimer = null
|
||
}
|
||
nativeWatchdogTimer = setTimeout(() => {
|
||
nativeWatchdogTimer = null
|
||
if (myGen !== tiltGen) return
|
||
if (mode !== 'native') return
|
||
console.warn(
|
||
'[useLenticularStudioTilt] native gyro stalled (no angle frames for ' +
|
||
NATIVE_GYRO_STALL_FALLBACK_MS +
|
||
'ms), falling back to accelerometer'
|
||
)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
}, NATIVE_GYRO_STALL_FALLBACK_MS)
|
||
}
|
||
|
||
function invokeStartNativeGyro() {
|
||
const mod = gyroModule
|
||
if (myGen !== tiltGen || !mod) return
|
||
|
||
function startNativePoll() {
|
||
if (nativePollTimer != null) {
|
||
try {
|
||
clearInterval(nativePollTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
nativePollTimer = null
|
||
}
|
||
/**
|
||
* 官方示例只用异步 getGyroValue;部分基座上 getGyroValueSync 长期无 x/y/z,
|
||
* 仅在没有异步接口时才用 Sync(见插件 id=6237 说明)。
|
||
*/
|
||
const useSync =
|
||
typeof mod.getGyroValueSync === 'function' && typeof mod.getGyroValue !== 'function'
|
||
const intervalMs = useSync ? 20 : 28
|
||
const pollOnce = () => {
|
||
if (myGen !== tiltGen || !gyroModule) return
|
||
try {
|
||
if (useSync) {
|
||
const v = mod.getGyroValueSync()
|
||
if (v && hasAnglePayload(v)) {
|
||
onNativeAngleFrame(v.x, v.y, v.z)
|
||
}
|
||
return
|
||
}
|
||
mod.getGyroValue((v) => {
|
||
if (myGen !== tiltGen || !gyroModule || !v) return
|
||
if (hasAnglePayload(v)) {
|
||
onNativeAngleFrame(v.x, v.y, v.z)
|
||
}
|
||
})
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
}
|
||
pollOnce()
|
||
nativePollTimer = setInterval(pollOnce, intervalMs)
|
||
}
|
||
|
||
let kickOnce = false
|
||
const kick = () => {
|
||
if (kickOnce) return
|
||
if (myGen !== tiltGen || !gyroModule) return
|
||
kickOnce = true
|
||
if (gyroCbGuardTimer != null) {
|
||
try {
|
||
clearTimeout(gyroCbGuardTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
gyroCbGuardTimer = null
|
||
}
|
||
|
||
const usePoll =
|
||
typeof mod.startGyro === 'function' &&
|
||
(typeof mod.getGyroValue === 'function' || typeof mod.getGyroValueSync === 'function')
|
||
|
||
if (usePoll) {
|
||
try {
|
||
mod.startGyro(startOpts, (res) => {
|
||
if (myGen !== tiltGen || !gyroModule) return
|
||
/* 插件约定:首包只表示是否开启成功,不含持续角度;若回调无对象则无法进入官方要求的 getGyroValue 轮询 */
|
||
if (!res) {
|
||
console.warn(
|
||
'[useLenticularStudioTilt] startGyro callback received no argument (see plugin doc: first callback is handshake only)'
|
||
)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
return
|
||
}
|
||
if (isExplicitGyroHandshakeFailure(res)) {
|
||
console.warn('[useLenticularStudioTilt] startGyro handshake failed', res)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
return
|
||
}
|
||
/* 与官方示例一致:success 为真后再轮询;iOS 上常为字符串 'true' */
|
||
if (Object.prototype.hasOwnProperty.call(res, 'success') && !isTruthyFlag(res.success)) {
|
||
console.warn('[useLenticularStudioTilt] startGyro success=false', res)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
return
|
||
}
|
||
if (hasAnglePayload(res)) {
|
||
onNativeAngleFrame(res.x, res.y, res.z)
|
||
} else {
|
||
simulate(0, 0)
|
||
}
|
||
startNativePoll()
|
||
})
|
||
} catch (e) {
|
||
console.warn('[useLenticularStudioTilt] startGyro error', e)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
}
|
||
return
|
||
}
|
||
|
||
try {
|
||
mod.startGyroWithCallback(startOpts, (res) => {
|
||
if (myGen !== tiltGen || !gyroModule) return
|
||
if (!res) {
|
||
console.warn('[useLenticularStudioTilt] startGyroWithCallback: empty callback argument')
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
return
|
||
}
|
||
|
||
if (!hasAnglePayload(res)) {
|
||
if (isExplicitGyroHandshakeFailure(res)) {
|
||
console.warn('[useLenticularStudioTilt] native gyro handshake failed', res)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
return
|
||
}
|
||
simulate(0, 0)
|
||
return
|
||
}
|
||
|
||
onNativeAngleFrame(res.x, res.y, res.z)
|
||
})
|
||
} catch (e) {
|
||
console.warn('[useLenticularStudioTilt] startGyroWithCallback error', e)
|
||
stopNative()
|
||
startAccelInternal({ fromNativeFallback: true })
|
||
}
|
||
}
|
||
|
||
if (typeof mod.getGyroStarted === 'function') {
|
||
gyroCbGuardTimer = setTimeout(() => {
|
||
gyroCbGuardTimer = null
|
||
if (myGen !== tiltGen || !gyroModule || kickOnce) return
|
||
console.warn('[useLenticularStudioTilt] getGyroStarted callback timeout, starting gyro')
|
||
kick()
|
||
}, 700)
|
||
mod.getGyroStarted((r) => {
|
||
if (gyroCbGuardTimer != null) {
|
||
try {
|
||
clearTimeout(gyroCbGuardTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
gyroCbGuardTimer = null
|
||
}
|
||
if (myGen !== tiltGen || !gyroModule) return
|
||
const started = r && isTruthyFlag(r.started)
|
||
if (started) {
|
||
let stopDone = false
|
||
const stopTimer = setTimeout(() => {
|
||
if (stopDone || myGen !== tiltGen || !gyroModule) return
|
||
stopDone = true
|
||
console.warn('[useLenticularStudioTilt] stopGyro callback timeout, continuing')
|
||
setTimeout(kick, 80)
|
||
}, 700)
|
||
mod.stopGyro(() => {
|
||
if (stopDone || myGen !== tiltGen || !gyroModule) return
|
||
stopDone = true
|
||
try {
|
||
clearTimeout(stopTimer)
|
||
} catch (e) {
|
||
/* noop */
|
||
}
|
||
setTimeout(kick, 80)
|
||
})
|
||
} else {
|
||
kick()
|
||
}
|
||
})
|
||
} else {
|
||
kick()
|
||
}
|
||
}
|
||
|
||
/* 官方:因 uni-app 原因,页面进入后需延时再 start(示例 100ms) */
|
||
nativeStartTimer = setTimeout(() => {
|
||
nativeStartTimer = null
|
||
if (myGen !== tiltGen || !gyroModule) return
|
||
invokeStartNativeGyro()
|
||
if (typeof scheduleNativeGyroStallFallback === 'function') {
|
||
scheduleNativeGyroStallFallback()
|
||
}
|
||
}, 100)
|
||
// #endif
|
||
// #ifndef APP-PLUS
|
||
mode = 'accel'
|
||
startAccelInternal()
|
||
// #endif
|
||
}
|
||
|
||
function start() {
|
||
// #ifdef APP-PLUS
|
||
startNativeInternal()
|
||
// #endif
|
||
// #ifndef APP-PLUS
|
||
startAccelInternal()
|
||
// #endif
|
||
}
|
||
|
||
function stop() {
|
||
stopNative()
|
||
stopAccel()
|
||
}
|
||
|
||
function recalibrate() {
|
||
resetAccelCalibration()
|
||
resetNativeBaseline()
|
||
resetStudioAccelBaseline()
|
||
accelSmoothed = 0
|
||
rollAccum = 0
|
||
prevCu = null
|
||
prevCv = null
|
||
simulate(0, 0)
|
||
}
|
||
|
||
return {
|
||
start,
|
||
stop,
|
||
recalibrate,
|
||
}
|
||
}
|