topfans/frontend/composables/useLenticularStudioTilt.js
2026-05-14 22:18:25 +08:00

769 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 光栅卡工作室倾斜驱动App 优先 imengyu-UniAndroidGyroDCloud 插件 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 / fastestgame≈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,
}
}