refactor(craft-tilt): 删除硬件传感器模块 + 同步清理 3 个其他页面
- 删 useLenticularStudioTilt.js (28KB, 894 行) - 删 useOneEuroFilter.js (75 行) - useLenticularCraftTiltPreview 移除对二者的引用与 3 个旧 API(scheduleTiltStart/stopTiltPreview/gyroSourceLabel) - 同步清理 3 个其他消费方页面(discover/generation-result, castlove/success, asset-detail): - 删 3 个旧 destructure 键 - 模板 :gyro-source 改静态 'simulation' + 删 :skip-built-in-touch - 删 12 处旧 API 调用 - 保留手动模拟入口 simulateTilt / simulateTiltFromNormalized / simulateTiltReset(Task 1) - ★ 3 个页面用触摸事件替代硬件传感器(未加 slider,后续可补) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
34dd301a92
commit
a4836605e7
@ -1,16 +1,12 @@
|
||||
/**
|
||||
* 铸爱光栅预览(lenticular-result / asset-detail 等):物理参数 + 陀螺仪倾斜 → 离散档位映射
|
||||
* 铸爱光栅预览(lenticular-result / asset-detail 等):手动倾斜入口 → 离散档位映射
|
||||
*
|
||||
* 引擎是连续叠化引擎(gamma -1..+1 → 层组叠化),但上层做硬切:
|
||||
* 1. 陀螺仪传感器 → filtered (dx, dy) 2D 向量
|
||||
* 1. 手动入口(滑块 / 触摸)→ 归一化 -1..+1 → 度数
|
||||
* 2. (dx, dy) → 2D 幅值 + 方向 → 离散 layerIdx(硬阈值 + 滞回 + 冷却)
|
||||
* 3. layerIdx → gamma ±1/0 → engine + snapSimulatedTilt 重置平滑
|
||||
*
|
||||
* 传感器数据处理见 useLenticularStudioTilt.js(跳变拒绝 / 快慢通道 / 符号滞回)。
|
||||
*/
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useLenticularPreview } from '@/composables/useLenticularPreview.js'
|
||||
import { useLenticularStudioTilt } from '@/composables/useLenticularStudioTilt.js'
|
||||
|
||||
// 手动模拟倾斜:归一化 → 度数 转换系数(spec §3.5)
|
||||
const NORMALIZED_TO_DEG = 45
|
||||
@ -48,7 +44,6 @@ const dlog = (...args) => {
|
||||
* @param {import('vue').Ref<Array>|import('vue').Ref<unknown>} layersRef
|
||||
*/
|
||||
export function useLenticularCraftTiltPreview(layersRef) {
|
||||
const gyroSourceLabel = ref('simulation')
|
||||
const { physics, layerTransforms, simulate, relax, snapSimulatedTilt } = useLenticularPreview(layersRef)
|
||||
Object.assign(physics, FLOW_PHYSICS)
|
||||
|
||||
@ -176,45 +171,6 @@ export function useLenticularCraftTiltPreview(layersRef) {
|
||||
simulate(0, 0)
|
||||
}
|
||||
|
||||
const { start: startTilt, stop: stopTilt } = useLenticularStudioTilt({
|
||||
simulate,
|
||||
simulateFromSignedDegrees,
|
||||
gyroSourceLabel,
|
||||
onTiltDriverFallback: () => {
|
||||
lastLayerIdx = -1
|
||||
dirSign = 0
|
||||
dirOppositeStreak = 0
|
||||
},
|
||||
})
|
||||
|
||||
let entryTiltTimer = null
|
||||
|
||||
function cancelScheduledTiltStart() {
|
||||
if (entryTiltTimer != null) {
|
||||
clearTimeout(entryTiltTimer)
|
||||
entryTiltTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTiltStart() {
|
||||
dlog('[scheduleTiltStart] called')
|
||||
cancelScheduledTiltStart()
|
||||
nextTick(() => {
|
||||
relax(0.86)
|
||||
lockPreviewStill()
|
||||
dlog('[scheduleTiltStart] timer set, delay:', LENTICULAR_TILT_START_DELAY_MS)
|
||||
entryTiltTimer = setTimeout(() => {
|
||||
entryTiltTimer = null
|
||||
startTilt()
|
||||
}, LENTICULAR_TILT_START_DELAY_MS)
|
||||
})
|
||||
}
|
||||
|
||||
function stopTiltPreview() {
|
||||
cancelScheduledTiltStart()
|
||||
stopTilt()
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 手动模拟倾斜入口(spec §3.1)
|
||||
// ====================================================================
|
||||
@ -253,9 +209,6 @@ export function useLenticularCraftTiltPreview(layersRef) {
|
||||
layerTransforms,
|
||||
simulate,
|
||||
relax,
|
||||
gyroSourceLabel,
|
||||
scheduleTiltStart,
|
||||
stopTiltPreview,
|
||||
lockPreviewStill,
|
||||
// 新增:手动模拟入口(spec §3.1)
|
||||
simulateTilt,
|
||||
|
||||
@ -1,840 +0,0 @@
|
||||
/**
|
||||
* 光栅卡倾斜驱动(铸爱预览共用)
|
||||
*
|
||||
* 多路径混合(按优先级):
|
||||
* - App Android:Native.js → SensorManager(加速度计低通滤波 → 重力方向 → 绝对倾角)
|
||||
* - App iOS:Native.js → CMMotionManager.deviceMotion.attitude(系统级传感器融合)
|
||||
* - H5:DeviceOrientationEvent(Chrome/Safari/Firefox 通用)
|
||||
* - 小程序:降级为模拟(无陀螺仪 API)
|
||||
*
|
||||
* ⚠️ uni-app 内置 uni.startGyroscope / uni.onGyroscopeChange 在 Android/iOS
|
||||
* 原生层未实现桥接——文档里写着但底层没接通。实测所有 3.x 版本 App 打包后调用均
|
||||
* 静默失败(无报错、无日志、无回调)。官方跳过陀螺仪是因为 iOS CMMotionManager
|
||||
* 和 Android SensorManager 在生命周期、权限模型、回调线程上差异太大。
|
||||
* 结论:App 端必须用 Native.js 或原生插件接入。
|
||||
*
|
||||
* 平台接入要点:
|
||||
* - Android:BODY_SENSORS 权限(Android 12+ 强制,仅 ACCESS_FINE_LOCATION 不够)
|
||||
* - iOS:NSMotionUsageDescription(Info.plist,否则 CMMotionManager 直接失败)
|
||||
* - 不能在 onLoad 阶段调用传感器——须等 plus.ready 后
|
||||
* - 务必检查传感器硬件:getDefaultSensor(TYPE_GYROSCOPE) 返回 null = 不支持
|
||||
*
|
||||
* DeviceOrientationEvent:
|
||||
* - gamma: 左右倾斜 -90°~+90°(平放=0)
|
||||
* - beta: 前后倾斜 -180°~+180°(竖直=0)
|
||||
* - iOS 13+ Safari 需 requestPermission(用户手势触发)
|
||||
*
|
||||
* 关键设计:
|
||||
* 1. 跳变拒绝:abs(raw-prev) > 15° → clamp,掐掉位移加速度伪影
|
||||
* 2. 快通道 α=0.25:约 0.2s 到 63%,离散切换更跟手
|
||||
* 3. 慢通道 α=0.08 + 冷启动基线=0:自适应用户"休息位"
|
||||
* 4. dx/dy = fast - slow → 上层 2D 挡位映射
|
||||
* 5. 跳过前 5 帧预热帧(传感器初始化噪声)
|
||||
*/
|
||||
|
||||
import { createOneEuroFilter, nowMs } from '@/composables/useOneEuroFilter.js'
|
||||
|
||||
const JUMP_REJECT_DEG = 15
|
||||
const SKIP_WARMUP_FRAMES = 5
|
||||
const FAST_ALPHA = 0.25
|
||||
const SLOW_ALPHA = 0.08
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {(x: number, y: number) => void} opts.simulate
|
||||
* @param {(dx: number, dy: number) => void} [opts.simulateFromSignedDegrees]
|
||||
* @param {import('vue').Ref<string>} opts.gyroSourceLabel
|
||||
* @param {() => void} [opts.onTiltDriverFallback]
|
||||
*/
|
||||
export function useLenticularStudioTilt(opts) {
|
||||
const { simulate, simulateFromSignedDegrees, gyroSourceLabel, onTiltDriverFallback } = opts
|
||||
|
||||
// ====================================================================
|
||||
// 通用状态
|
||||
// ====================================================================
|
||||
let tiltGen = 0
|
||||
|
||||
// 跳变拒绝:上一帧 raw,NaN 表示"无条件接受第一帧"
|
||||
let prevRawX = NaN, prevRawY = NaN
|
||||
// 预热跳过计数器
|
||||
let warmupSkip = 0
|
||||
|
||||
// 快通道:平滑跟踪(α=0.25 跟手 + 抑制噪声)
|
||||
let fastX = NaN, fastY = NaN
|
||||
// 慢通道:缓慢追踪"休息位"(α=0.08 自动重居中,冷启动基线=0)
|
||||
let slowX = NaN, slowY = NaN
|
||||
|
||||
// 日志节流
|
||||
let lastLogSecond = 0
|
||||
|
||||
// ====================================================================
|
||||
// Native.js 路径状态
|
||||
// ====================================================================
|
||||
// #ifdef APP-PLUS
|
||||
let nativeCleanup = null // () => void
|
||||
let nativeStartTimer = null // setTimeout ID
|
||||
// #endif
|
||||
|
||||
// ====================================================================
|
||||
// DeviceOrientation 路径状态
|
||||
// ====================================================================
|
||||
let orientationHandler = null
|
||||
|
||||
// ——— 通用复位 ———
|
||||
|
||||
function resetState() {
|
||||
prevRawX = NaN; prevRawY = NaN
|
||||
warmupSkip = 0
|
||||
fastX = NaN; fastY = NaN
|
||||
slowX = NaN; slowY = NaN
|
||||
}
|
||||
|
||||
// ——— 跳变拒绝 ———
|
||||
|
||||
/**
|
||||
* 单帧 raw 变化超过 JUMP_REJECT_DEG 则 clamp,掐掉位移加速度伪影。
|
||||
* 真实倾斜不太可能超过 ±15°/帧,大跳变必定是设备平移加速度的伪信号。
|
||||
*/
|
||||
function clampJump(rawX, rawY) {
|
||||
let cx = rawX, cy = rawY
|
||||
if (Number.isFinite(prevRawX)) {
|
||||
if (Math.abs(rawX - prevRawX) > JUMP_REJECT_DEG) {
|
||||
cx = prevRawX + Math.sign(rawX - prevRawX) * JUMP_REJECT_DEG
|
||||
}
|
||||
if (Math.abs(rawY - prevRawY) > JUMP_REJECT_DEG) {
|
||||
cy = prevRawY + Math.sign(rawY - prevRawY) * JUMP_REJECT_DEG
|
||||
}
|
||||
}
|
||||
prevRawX = rawX; prevRawY = rawY
|
||||
return { cx, cy }
|
||||
}
|
||||
|
||||
// ——— 快慢双通道 ———
|
||||
|
||||
function updateFast(rawX, rawY) {
|
||||
if (!Number.isFinite(fastX)) { fastX = rawX; fastY = rawY; return }
|
||||
fastX += (rawX - fastX) * FAST_ALPHA
|
||||
fastY += (rawY - fastY) * FAST_ALPHA
|
||||
}
|
||||
|
||||
function updateSlow(rawX, rawY) {
|
||||
if (!Number.isFinite(slowX)) {
|
||||
// 冷启动基线=0:让第1帧 dx 立即有值(设备已倾斜时不会白板)
|
||||
slowX = 0; slowY = 0
|
||||
return
|
||||
}
|
||||
slowX += (rawX - slowX) * SLOW_ALPHA
|
||||
slowY += (rawY - slowY) * SLOW_ALPHA
|
||||
}
|
||||
|
||||
// ——— 统一输出 ———
|
||||
|
||||
function applyDelta(dx, dy) {
|
||||
// 节流日志(每秒最多一条)
|
||||
if (Math.floor(Date.now() / 1000) !== lastLogSecond) {
|
||||
lastLogSecond = Math.floor(Date.now() / 1000)
|
||||
console.log('[StudioTilt] dx:', dx.toFixed(1), 'dy:', dy.toFixed(1),
|
||||
'src:', gyroSourceLabel.value)
|
||||
}
|
||||
|
||||
if (typeof simulateFromSignedDegrees === 'function') {
|
||||
simulateFromSignedDegrees(dx, dy)
|
||||
} else {
|
||||
simulate(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Native.js 倾斜帧入口(Android & iOS 共用)
|
||||
// 参数:(rollDeg, pitchDeg) — 绝对倾角,单位为度
|
||||
// rollDeg: 左右倾斜(>0 右侧低),映射到 dx
|
||||
// pitchDeg: 前后倾斜(>0 后端低),映射到 dy
|
||||
// ====================================================================
|
||||
|
||||
function handleNativeTiltFrame(rollDeg, pitchDeg) {
|
||||
if (warmupSkip < SKIP_WARMUP_FRAMES) {
|
||||
warmupSkip++
|
||||
return
|
||||
}
|
||||
|
||||
const { cx, cy } = clampJump(rollDeg, pitchDeg)
|
||||
updateFast(cx, cy)
|
||||
updateSlow(cx, cy)
|
||||
|
||||
const dx = fastX - slowX
|
||||
const dy = fastY - slowY
|
||||
|
||||
applyDelta(dx, dy)
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 路径 A-1: Native.js Android
|
||||
// 用加速度计 + 低通滤波提取重力方向 → 绝对 roll/pitch(度)
|
||||
// 避免直接积分陀螺仪(TYPE_GYROSCOPE 输出角速度 rad/s,积分漂移严重)
|
||||
// ====================================================================
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
|
||||
// ================================================================
|
||||
// 插件路径 (Android):imengyu-UniAndroidGyro-GyroModule
|
||||
// 插件内部完成 Java float[] → JS number 转换,绕开 Native.js 限制
|
||||
// ================================================================
|
||||
|
||||
const NATIVE_PLUGIN_ID = 'imengyu-UniAndroidGyro-GyroModule'
|
||||
const POLL_INTERVAL_MS = 33
|
||||
const RESTART_DELAY_MS = 600
|
||||
const STARTGYRO_MAX_RETRIES = 3
|
||||
const STARTGYRO_BASE_RETRY_MS = 400
|
||||
|
||||
let gyroModule = null
|
||||
let gyroPollGen = 0
|
||||
let pollTimer = null
|
||||
let startTimer = null
|
||||
|
||||
function tryRequireGyroModule() {
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && typeof uni.requireNativePlugin === 'function') {
|
||||
const mod = uni.requireNativePlugin(NATIVE_PLUGIN_ID)
|
||||
if (mod && typeof mod.startGyro === 'function' && typeof mod.stopGyro === 'function') {
|
||||
return mod
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[useLenticularStudioTilt] requireNativePlugin failed', e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (pollTimer != null) {
|
||||
try { clearInterval(pollTimer) } catch (_) {}
|
||||
pollTimer = null
|
||||
}
|
||||
if (startTimer != null) {
|
||||
try { clearTimeout(startTimer) } catch (_) {}
|
||||
startTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleGyroValue(res) {
|
||||
const rawX = Number(res.y || 0) // roll → dx(左右)
|
||||
const rawY = Number(res.x || 0) // pitch → dy(前后)
|
||||
|
||||
if (warmupSkip < SKIP_WARMUP_FRAMES) {
|
||||
warmupSkip++
|
||||
return
|
||||
}
|
||||
|
||||
const { cx, cy } = clampJump(rawX, rawY)
|
||||
updateFast(cx, cy)
|
||||
updateSlow(cx, cy)
|
||||
|
||||
const dx = fastX - slowX
|
||||
const dy = fastY - slowY
|
||||
|
||||
applyDelta(dx, dy)
|
||||
}
|
||||
|
||||
function pollOnce(myGen) {
|
||||
if (myGen !== gyroPollGen || !gyroModule) return
|
||||
try {
|
||||
gyroModule.getGyroValue((res) => {
|
||||
if (myGen !== gyroPollGen || !gyroModule || !res) return
|
||||
gyroSourceLabel.value = 'gyroscope'
|
||||
handleGyroValue(res)
|
||||
})
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function tryStartGyro(myGen, retry) {
|
||||
if (myGen !== gyroPollGen || !gyroModule) return
|
||||
|
||||
if (retry > 0) {
|
||||
console.log('[useLenticularStudioTilt] startGyro retry #', retry)
|
||||
}
|
||||
|
||||
gyroModule.startGyro({ interval: 'ui' }, (res) => {
|
||||
if (myGen !== gyroPollGen || !gyroModule) return
|
||||
|
||||
if (!res || !res.success) {
|
||||
const errMsg = (res && res.errMsg) || ''
|
||||
if (retry < STARTGYRO_MAX_RETRIES && /running/i.test(errMsg)) {
|
||||
const delay = STARTGYRO_BASE_RETRY_MS * (retry + 1)
|
||||
console.warn('[useLenticularStudioTilt] startGyro busy, retry in', delay, 'ms')
|
||||
startTimer = setTimeout(() => {
|
||||
startTimer = null
|
||||
tryStartGyro(myGen, retry + 1)
|
||||
}, delay)
|
||||
return
|
||||
}
|
||||
console.warn('[useLenticularStudioTilt] startGyro failed:', res)
|
||||
onNativeFallback()
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[useLenticularStudioTilt] plugin gyro ok, polling')
|
||||
gyroSourceLabel.value = 'gyroscope'
|
||||
warmupSkip = 0
|
||||
pollOnce(myGen)
|
||||
pollTimer = setInterval(() => pollOnce(myGen), POLL_INTERVAL_MS)
|
||||
})
|
||||
}
|
||||
|
||||
function stopGyroPlugin() {
|
||||
gyroPollGen++
|
||||
stopPoll()
|
||||
if (gyroModule && typeof gyroModule.stopGyro === 'function') {
|
||||
try { gyroModule.stopGyro(() => {}) } catch (_) {}
|
||||
}
|
||||
gyroModule = null
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 原生传感器 (iOS / Android 备用)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param {(rollDeg: number, pitchDeg: number) => void} onFrame
|
||||
* @returns {{ cleanup: () => void } | null}
|
||||
*/
|
||||
function createAndroidTiltSensor(onFrame) {
|
||||
try {
|
||||
if (typeof plus === 'undefined' || !plus.android) {
|
||||
console.warn('[useLenticularStudioTilt] plus.android not available')
|
||||
return null
|
||||
}
|
||||
|
||||
const Context = plus.android.importClass('android.content.Context')
|
||||
const SensorManager = plus.android.importClass('android.hardware.SensorManager')
|
||||
const Sensor = plus.android.importClass('android.hardware.Sensor')
|
||||
|
||||
const activity = plus.android.runtimeMainActivity()
|
||||
const sm = activity.getSystemService(Context.SENSOR_SERVICE)
|
||||
|
||||
// 先检查陀螺仪硬件是否存在(作为诊断日志,实际用加速度计)
|
||||
const gyroSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||
if (!gyroSensor) {
|
||||
console.warn('[useLenticularStudioTilt] device has no gyroscope hardware — ' +
|
||||
'using accelerometer gravity fallback')
|
||||
} else {
|
||||
console.log('[useLenticularStudioTilt] gyroscope hardware detected; using accelerometer gravity for absolute tilt')
|
||||
}
|
||||
|
||||
// 检查加速度计
|
||||
const gravitySensor = sm.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
const accelSensorRaw = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
console.log('[useLenticularStudioTilt] Android sensor support: ' +
|
||||
'gyro=' + (gyroSensor ? 'YES' : 'NO') + ', ' +
|
||||
'gravity=' + (gravitySensor ? 'YES' : 'NO') + ', ' +
|
||||
'accel=' + (accelSensorRaw ? 'YES' : 'NO'))
|
||||
|
||||
// TYPE_GRAVITY 直接输出重力分量,优先使用;不支持则用 TYPE_ACCELEROMETER + LPF
|
||||
let accelSensor = gravitySensor
|
||||
const useGravitySensor = !!accelSensor
|
||||
if (!accelSensor) {
|
||||
accelSensor = accelSensorRaw
|
||||
console.log('[useLenticularStudioTilt] TYPE_GRAVITY unavailable, using TYPE_ACCELEROMETER + LPF')
|
||||
} else {
|
||||
console.log('[useLenticularStudioTilt] using TYPE_GRAVITY sensor (no LPF needed)')
|
||||
}
|
||||
if (!accelSensor) {
|
||||
console.warn('[useLenticularStudioTilt] no accelerometer available — tilt NOT supported on this device')
|
||||
return null
|
||||
}
|
||||
|
||||
// 低通滤波状态(仅 TYPE_ACCELEROMETER 需要)
|
||||
const LPF_ALPHA = 0.85
|
||||
let gx = 0, gy = 0, gz = 0
|
||||
let gravityReady = useGravitySensor // TYPE_GRAVITY 不需要 LPF
|
||||
|
||||
let sensorCallbackCount = 0
|
||||
let ArrayReflect = null
|
||||
|
||||
const listener = plus.android.implements('android.hardware.SensorEventListener', {
|
||||
onSensorChanged: function(event) {
|
||||
sensorCallbackCount++
|
||||
try {
|
||||
// ——— 诊断:探明 event 对象的实际结构 ———
|
||||
if (sensorCallbackCount <= 3) {
|
||||
// 枚举 event 上的所有 key
|
||||
let keys = []
|
||||
try {
|
||||
for (let k in event) {
|
||||
try { keys.push(k + ':' + typeof event[k]) } catch (_) { keys.push(k + ':err') }
|
||||
}
|
||||
} catch (_) {}
|
||||
console.log('[useLenticularStudioTilt] onSensorChanged #' + sensorCallbackCount +
|
||||
' typeof=' + typeof event +
|
||||
' keys=[' + (keys.length ? keys.join(', ') : '(none)') + ']')
|
||||
|
||||
// 尝试 plusGetAttribute 方式(项目既有模式)
|
||||
try {
|
||||
const v = event.plusGetAttribute('values')
|
||||
console.log('[useLenticularStudioTilt] plusGetAttribute("values"):', typeof v, v)
|
||||
} catch (e1) {
|
||||
console.log('[useLenticularStudioTilt] plusGetAttribute("values") threw:', e1)
|
||||
}
|
||||
|
||||
// 尝试 plus.android.getAttribute
|
||||
try {
|
||||
const v = plus.android.getAttribute(event, 'values')
|
||||
console.log('[useLenticularStudioTilt] getAttribute(event,"values"):', typeof v, v)
|
||||
} catch (e2) {
|
||||
console.log('[useLenticularStudioTilt] getAttribute(event,"values") threw:', e2)
|
||||
}
|
||||
}
|
||||
|
||||
// ——— 获取 float[] 数据 ———
|
||||
// 已验证:
|
||||
// ✅ plusGetAttribute('values') → Java float[]
|
||||
// ✅ Array.get(arr, i) → Java Float 对象(之前 r0 type=object 是成功的)
|
||||
// ❌ Array.getFloat(arr, i) → null
|
||||
// ❌ Number(Float) → 0
|
||||
// ❌ Float.floatValue 不是 JS function
|
||||
// 取 float[] 数据 — 内联调用,不经过函数传参(Native.js 传参丢引用)
|
||||
let v0, v1, v2
|
||||
|
||||
let rawValues = null
|
||||
try { rawValues = event.plusGetAttribute('values') } catch (_) {}
|
||||
if (!rawValues) {
|
||||
try { rawValues = plus.android.getAttribute(event, 'values') } catch (_) {}
|
||||
}
|
||||
if (!rawValues) return
|
||||
|
||||
if (!ArrayReflect || typeof ArrayReflect.get !== "function") {
|
||||
try {
|
||||
ArrayReflect = plus.android.importClass("java.lang.reflect.Array")
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 内联 Array.get()(已验证可用),拿 Float → invoke floatValue 拆箱
|
||||
if (sensorCallbackCount <= 2) { console.log('[useLenticularStudioTilt] pre-get: ArrayReflect=' + typeof ArrayReflect + ' hasGet=' + (ArrayReflect && typeof ArrayReflect.get === 'function') + ' rv=' + typeof rawValues + ' rvNull=' + (rawValues == null)) }
|
||||
const f0 = ArrayReflect ? ArrayReflect.get(rawValues, 0) : null
|
||||
const f1 = ArrayReflect ? ArrayReflect.get(rawValues, 1) : null
|
||||
const f2 = ArrayReflect ? ArrayReflect.get(rawValues, 2) : null
|
||||
|
||||
if (sensorCallbackCount <= 2) {
|
||||
console.log('[useLenticularStudioTilt] Array.get results: f0=' + typeof f0 +
|
||||
' null=' + (f0 == null) + ' f1 null=' + (f1 == null) + ' f2 null=' + (f2 == null))
|
||||
}
|
||||
|
||||
function unboxFloat(f) {
|
||||
if (f == null) return NaN
|
||||
try {
|
||||
const v = plus.android.invoke(f, 'floatValue')
|
||||
if (typeof v === 'number') return v
|
||||
} catch (_) {}
|
||||
try {
|
||||
const s = plus.android.invoke(f, 'toString')
|
||||
if (s) return parseFloat(String(s))
|
||||
} catch (_) {}
|
||||
return NaN
|
||||
}
|
||||
|
||||
v0 = unboxFloat(f0)
|
||||
v1 = unboxFloat(f1)
|
||||
v2 = unboxFloat(f2)
|
||||
|
||||
if (typeof v0 !== 'number' || isNaN(v0)) {
|
||||
if (sensorCallbackCount <= 3) {
|
||||
console.warn('[useLenticularStudioTilt] onSensorChanged: failed to read values #' + sensorCallbackCount)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 第一次拿到值时打印
|
||||
if (sensorCallbackCount <= 5) {
|
||||
console.log('[useLenticularStudioTilt] sensor #' + sensorCallbackCount +
|
||||
': [' + v0.toFixed(3) + ', ' + v1.toFixed(3) + ', ' + v2.toFixed(3) + ']')
|
||||
}
|
||||
|
||||
let rx, ry, rz
|
||||
|
||||
if (useGravitySensor) {
|
||||
// TYPE_GRAVITY 直接输出重力分量
|
||||
rx = v0
|
||||
ry = v1
|
||||
rz = v2
|
||||
} else {
|
||||
// TYPE_ACCELEROMETER:低通滤波提取重力分量
|
||||
gx = LPF_ALPHA * gx + (1 - LPF_ALPHA) * v0
|
||||
gy = LPF_ALPHA * gy + (1 - LPF_ALPHA) * v1
|
||||
gz = LPF_ALPHA * gz + (1 - LPF_ALPHA) * v2
|
||||
|
||||
if (!gravityReady) {
|
||||
// 等重力滤波器收敛后再输出
|
||||
const mag = Math.sqrt(gx * gx + gy * gy + gz * gz)
|
||||
if (mag > 8 && mag < 12) gravityReady = true
|
||||
return
|
||||
}
|
||||
rx = gx; ry = gy; rz = gz
|
||||
}
|
||||
|
||||
// 重力方向 → roll/pitch(度),坐标系对齐 DeviceOrientation
|
||||
// roll: 绕 z 轴旋转(左右倾斜)
|
||||
// pitch: 绕 x 轴旋转(前后倾斜)
|
||||
const roll = Math.atan2(rx, rz) * (180 / Math.PI)
|
||||
const pitch = Math.atan2(ry, Math.sqrt(rx * rx + rz * rz)) * (180 / Math.PI)
|
||||
|
||||
onFrame(roll, pitch)
|
||||
} catch (e) {
|
||||
if (sensorCallbackCount <= 3) {
|
||||
console.warn('[useLenticularStudioTilt] onSensorChanged error:', e, JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
},
|
||||
onAccuracyChanged: function(sensor, accuracy) {}
|
||||
})
|
||||
|
||||
// SENSOR_DELAY_GAME = 20,000μs (50Hz) — 响应快,够跟手
|
||||
sm.registerListener(listener, accelSensor, SensorManager.SENSOR_DELAY_GAME)
|
||||
|
||||
return {
|
||||
cleanup: function() {
|
||||
sm.unregisterListener(listener)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[useLenticularStudioTilt] Android tilt sensor init failed:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 路径 A-2: Native.js iOS
|
||||
// CMMotionManager.deviceMotion.attitude → roll/pitch(度)
|
||||
// 系统级传感器融合,无漂移,比手动积分陀螺仪可靠得多
|
||||
// ====================================================================
|
||||
|
||||
/**
|
||||
* @param {(rollDeg: number, pitchDeg: number) => void} onFrame
|
||||
* @returns {{ cleanup: () => void } | null}
|
||||
*/
|
||||
function createIOSTiltSensor(onFrame) {
|
||||
try {
|
||||
if (typeof plus === 'undefined' || !plus.ios) {
|
||||
console.warn('[useLenticularStudioTilt] plus.ios not available')
|
||||
return null
|
||||
}
|
||||
|
||||
const CMMotionManager = plus.ios.importClass('CMMotionManager')
|
||||
const NSOperationQueue = plus.ios.importClass('NSOperationQueue')
|
||||
|
||||
const mm = CMMotionManager.alloc().init()
|
||||
|
||||
if (!mm || !mm.isDeviceMotionAvailable || !mm.isDeviceMotionAvailable()) {
|
||||
console.warn('[useLenticularStudioTilt] CMMotionManager deviceMotion NOT available — tilt NOT supported on this device')
|
||||
if (mm) { mm.release() }
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查各传感器硬件可用性(诊断用)
|
||||
const gyroOk = mm.isGyroAvailable && mm.isGyroAvailable()
|
||||
const accelOk = mm.isAccelerometerAvailable && mm.isAccelerometerAvailable()
|
||||
const magnetoOk = mm.isMagnetometerAvailable && mm.isMagnetometerAvailable()
|
||||
console.log('[useLenticularStudioTilt] iOS sensor support: ' +
|
||||
'deviceMotion=YES, ' +
|
||||
'gyro=' + (gyroOk ? 'YES' : 'NO') + ', ' +
|
||||
'accel=' + (accelOk ? 'YES' : 'NO') + ', ' +
|
||||
'magnetometer=' + (magnetoOk ? 'YES' : 'NO'))
|
||||
if (!gyroOk) {
|
||||
console.warn('[useLenticularStudioTilt] iOS gyro unavailable; deviceMotion will fall back to accel+compass fusion')
|
||||
}
|
||||
|
||||
// 30Hz 采样(适合倾斜交互,低于游戏需求但省电)
|
||||
mm.setDeviceMotionUpdateInterval(1.0 / 30.0)
|
||||
|
||||
const queue = NSOperationQueue.mainQueue()
|
||||
|
||||
// 用 block 接收 motion 数据
|
||||
// CMDeviceMotionHandler = void (^)(CMDeviceMotion * _Nullable motion, NSError * _Nullable error)
|
||||
const handler = plus.ios.implements('CMDeviceMotionHandler', {
|
||||
invoke: function(motion, error) {
|
||||
try {
|
||||
if (!motion || error) return
|
||||
const attitude = motion.attitude()
|
||||
if (!attitude) return
|
||||
// attitude.roll / pitch 是弧度,转度
|
||||
const roll = attitude.roll() * (180 / Math.PI)
|
||||
const pitch = attitude.pitch() * (180 / Math.PI)
|
||||
onFrame(roll, pitch)
|
||||
} catch (_) {}
|
||||
}
|
||||
})
|
||||
|
||||
mm.startDeviceMotionUpdatesToQueueWithHandler(queue, handler)
|
||||
|
||||
return {
|
||||
cleanup: function() {
|
||||
mm.stopDeviceMotionUpdates()
|
||||
mm.release()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[useLenticularStudioTilt] iOS tilt sensor init failed:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 plus 环境就绪后执行回调。
|
||||
* 在 Vue mounted 阶段调用时 plus 通常已就绪,但加一层防护。
|
||||
* @param {(ready: boolean) => void} cb
|
||||
*/
|
||||
function onPlusReady(cb) {
|
||||
try {
|
||||
if (typeof plus !== 'undefined') {
|
||||
cb(true)
|
||||
return
|
||||
}
|
||||
// 尚未就绪:挂载 plusready 事件
|
||||
function handler() {
|
||||
document.removeEventListener('plusready', handler)
|
||||
cb(true)
|
||||
}
|
||||
document.addEventListener('plusready', handler, false)
|
||||
} catch (_) {
|
||||
cb(false)
|
||||
}
|
||||
}
|
||||
|
||||
function startNativeApp(myGen) {
|
||||
onPlusReady(function(ready) {
|
||||
if (!ready) {
|
||||
console.warn('[useLenticularStudioTilt] plus not ready')
|
||||
onNativeFallback()
|
||||
return
|
||||
}
|
||||
|
||||
// Android:uni.onAccelerometerChange(uni 原生桥接,不走 WebView 事件,
|
||||
// 也不走 Native.js float[] 读取,uni 内部处理数据转换,直接返 JS number)。
|
||||
if (plus.os && plus.os.name === 'Android') {
|
||||
uni.startAccelerometer({ interval: 'game' })
|
||||
let accelWarmup = 0
|
||||
// ——— Android-accel 路径专用:One-Euro Filter(速度自适应低通)———
|
||||
// 必须在闭包内新建,不能放在 handleNativeTiltFrame
|
||||
// (handleNativeTiltFrame 被 iOS CMMotionManager 共用)
|
||||
const oneEuroRoll = createOneEuroFilter({ mincutoff: 0.2, beta: 0.05, dcutoff: 1.0 })
|
||||
const oneEuroPitch = createOneEuroFilter({ mincutoff: 0.2, beta: 0.05, dcutoff: 1.0 })
|
||||
|
||||
let accelCb = function(res) {
|
||||
if (myGen !== tiltGen) return
|
||||
// 跳过前几帧初始化噪声
|
||||
if (accelWarmup < SKIP_WARMUP_FRAMES) { accelWarmup++; return }
|
||||
|
||||
const rollRaw = Math.atan2(res.x, res.z) * (180 / Math.PI)
|
||||
const pitchRaw = Math.atan2(res.y, Math.sqrt(res.x*res.x + res.z*res.z)) * (180 / Math.PI)
|
||||
|
||||
// One-Euro 平滑(解决静止抖动 + 减小动态延迟)
|
||||
const t = nowMs()
|
||||
const roll = oneEuroRoll.filter(rollRaw, t)
|
||||
const pitch = oneEuroPitch.filter(pitchRaw, t)
|
||||
|
||||
gyroSourceLabel.value = 'accelerometer'
|
||||
handleNativeTiltFrame(roll, pitch)
|
||||
}
|
||||
uni.onAccelerometerChange(accelCb)
|
||||
nativeCleanup = function() {
|
||||
uni.offAccelerometerChange(accelCb)
|
||||
try { uni.stopAccelerometer() } catch (_) {}
|
||||
}
|
||||
console.log('[useLenticularStudioTilt] uni.onAccelerometerChange started with One-Euro Filter')
|
||||
return
|
||||
}
|
||||
|
||||
// iOS:CMMotionManager.attitude
|
||||
if (plus.os && plus.os.name === 'iOS') {
|
||||
const result = createIOSTiltSensor(function(roll, pitch) {
|
||||
if (myGen !== tiltGen) return
|
||||
gyroSourceLabel.value = 'native-ios'
|
||||
handleNativeTiltFrame(roll, pitch)
|
||||
})
|
||||
if (result) {
|
||||
nativeCleanup = result.cleanup
|
||||
console.log('[useLenticularStudioTilt] Native.js iOS tilt started')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Native.js 不可用 → 降级
|
||||
console.warn('[useLenticularStudioTilt] Native.js tilt init failed for ' +
|
||||
(plus.os ? plus.os.name : 'unknown') + ' — falling back')
|
||||
onNativeFallback()
|
||||
})
|
||||
}
|
||||
|
||||
function stopNativeApp() {
|
||||
stopGyroPlugin()
|
||||
if (nativeStartTimer != null) {
|
||||
try { clearTimeout(nativeStartTimer) } catch (_) {}
|
||||
nativeStartTimer = null
|
||||
}
|
||||
if (nativeCleanup) {
|
||||
try { nativeCleanup() } catch (_) {}
|
||||
nativeCleanup = null
|
||||
}
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* Native.js 路径降级回调:转到 DeviceOrientation 或模拟
|
||||
*/
|
||||
function onNativeFallback() {
|
||||
// #ifdef APP-PLUS
|
||||
nativeCleanup = null
|
||||
// #endif
|
||||
|
||||
// 尝试 DeviceOrientationEvent
|
||||
console.log('[useLenticularStudioTilt] Native.js unavailable, trying DeviceOrientationEvent...')
|
||||
if (startOrientation()) return
|
||||
|
||||
// 全部不可用 → 模拟
|
||||
console.warn('[useLenticularStudioTilt] all tilt paths exhausted — falling back to simulation')
|
||||
gyroSourceLabel.value = 'simulation'
|
||||
simulate(0, 0)
|
||||
if (typeof onTiltDriverFallback === 'function') {
|
||||
try { onTiltDriverFallback() } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 路径 B: DeviceOrientationEvent (H5 / iOS App WKWebView 降级)
|
||||
// ====================================================================
|
||||
|
||||
function handleDeviceOrientation(e) {
|
||||
if (e.gamma == null) return
|
||||
|
||||
// gamma: 左右倾斜(roll),beta: 前后倾斜(pitch)
|
||||
const rawX = e.gamma || 0 // roll → dx(左右)
|
||||
const rawY = e.beta || 0 // pitch → dy(前后)
|
||||
|
||||
if (warmupSkip < SKIP_WARMUP_FRAMES) {
|
||||
warmupSkip++
|
||||
return
|
||||
}
|
||||
|
||||
const { cx, cy } = clampJump(rawX, rawY)
|
||||
updateFast(cx, cy)
|
||||
updateSlow(cx, cy)
|
||||
|
||||
const dx = fastX - slowX
|
||||
const dy = fastY - slowY
|
||||
|
||||
applyDelta(dx, dy)
|
||||
}
|
||||
|
||||
function startOrientationListener() {
|
||||
gyroSourceLabel.value = 'deviceorientation'
|
||||
warmupSkip = 0
|
||||
orientationHandler = handleDeviceOrientation
|
||||
window.addEventListener('deviceorientation', orientationHandler, true)
|
||||
console.log('[useLenticularStudioTilt] deviceorientation listener started')
|
||||
}
|
||||
|
||||
function stopOrientationListener() {
|
||||
if (orientationHandler) {
|
||||
window.removeEventListener('deviceorientation', orientationHandler, true)
|
||||
orientationHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试启动 DeviceOrientationEvent。
|
||||
* @returns {boolean} true=已启动或正在请求权限;false=不可用
|
||||
*/
|
||||
function startOrientation() {
|
||||
if (typeof DeviceOrientationEvent === 'undefined') {
|
||||
console.warn('[useLenticularStudioTilt] DeviceOrientationEvent not supported')
|
||||
return false
|
||||
}
|
||||
|
||||
// iOS 13+ Safari 需要用户手势触发 requestPermission
|
||||
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
|
||||
gyroSourceLabel.value = 'deviceorientation-requesting'
|
||||
DeviceOrientationEvent.requestPermission()
|
||||
.then(state => {
|
||||
if (state === 'granted') {
|
||||
startOrientationListener()
|
||||
} else {
|
||||
console.warn('[useLenticularStudioTilt] DeviceOrientation permission denied')
|
||||
gyroSourceLabel.value = 'simulation'
|
||||
simulate(0, 0)
|
||||
if (typeof onTiltDriverFallback === 'function') {
|
||||
try { onTiltDriverFallback() } catch (_) {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('[useLenticularStudioTilt] DeviceOrientation permission error:', err)
|
||||
gyroSourceLabel.value = 'simulation'
|
||||
simulate(0, 0)
|
||||
if (typeof onTiltDriverFallback === 'function') {
|
||||
try { onTiltDriverFallback() } catch (_) {}
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// Android / 旧 iOS:直接启动 listener
|
||||
startOrientationListener()
|
||||
return true
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 公开 API
|
||||
// ====================================================================
|
||||
|
||||
/**
|
||||
* 启动传感器。优先级:
|
||||
* 1. Native.js (App Android/iOS) — 直接桥接原生 API,最可靠
|
||||
* 2. DeviceOrientationEvent (H5 / iOS App WKWebView)
|
||||
* 3. 降级模拟
|
||||
*/
|
||||
function start() {
|
||||
console.log('[useLenticularStudioTilt] start()')
|
||||
stop()
|
||||
tiltGen++
|
||||
resetState()
|
||||
lastLogSecond = 0
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// App 端:优先 Native.js
|
||||
startNativeApp(tiltGen)
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
// H5 / 小程序:尝试 DeviceOrientationEvent
|
||||
if (startOrientation()) return
|
||||
|
||||
// 全部不可用 → 模拟
|
||||
gyroSourceLabel.value = 'simulation'
|
||||
simulate(0, 0)
|
||||
if (typeof onTiltDriverFallback === 'function') {
|
||||
try { onTiltDriverFallback() } catch (_) {}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
function stop() {
|
||||
console.log('[useLenticularStudioTilt] stop()')
|
||||
tiltGen++
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
stopNativeApp()
|
||||
// #endif
|
||||
|
||||
stopOrientationListener()
|
||||
|
||||
gyroSourceLabel.value = 'simulation'
|
||||
resetState()
|
||||
}
|
||||
|
||||
function recalibrate() {
|
||||
resetState()
|
||||
simulate(0, 0)
|
||||
}
|
||||
|
||||
return { start, stop, recalibrate }
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
@ -47,7 +47,7 @@
|
||||
</image>
|
||||
<view v-if="isLenticularAsset" class="detail-lenticular-slot">
|
||||
<LenticularCard class="detail-lenticular-card" :layers="lenticularLayers"
|
||||
:transforms="layerTransforms" :gyro-source="gyroSourceLabel" :skip-built-in-touch="true"
|
||||
:transforms="layerTransforms" gyro-source="simulation"
|
||||
tilt-hint-text="倾斜手机查看光栅效果" :shimmer-mid-opacity="0.16" @simulate="simulate" />
|
||||
</view>
|
||||
<image v-else class="card-image" :src="coverUrl" mode="aspectFill"></image>
|
||||
@ -274,9 +274,6 @@ const lenticularLayers = ref([]);
|
||||
const {
|
||||
layerTransforms,
|
||||
simulate,
|
||||
gyroSourceLabel,
|
||||
scheduleTiltStart,
|
||||
stopTiltPreview,
|
||||
} = useLenticularCraftTiltPreview(lenticularLayers);
|
||||
|
||||
const isLenticularAsset = computed(() => {
|
||||
@ -646,7 +643,6 @@ const loadData = async () => {
|
||||
? buildLenticularLayersTwo(bgUrl, subjectUrl)
|
||||
: buildLenticularLayers(subjectUrl)
|
||||
isLenticularDetail.value = true
|
||||
scheduleTiltStart()
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('加载封面图片失败:', err);
|
||||
@ -750,7 +746,6 @@ const loadCraftConfirm = () => {
|
||||
if (raw) {
|
||||
const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
lenticularLayers.value = buildLenticularLayersTwo(p.bgPath || '', p.subjectPath || '');
|
||||
scheduleTiltStart();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@ -871,12 +866,6 @@ onShow(() => {
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
if (craftConfirmMode.value && isCraftLenticular.value) {
|
||||
stopTiltPreview();
|
||||
}
|
||||
if (isLenticularDetail.value) {
|
||||
stopTiltPreview()
|
||||
}
|
||||
stopLikePolling();
|
||||
});
|
||||
|
||||
@ -888,7 +877,6 @@ onUnload(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
stopTiltPreview();
|
||||
stopLikePolling();
|
||||
uni.$off('assetLikeChanged', handleAssetLikeChangedExternal);
|
||||
});
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<view class="craft-lenticular-slot">
|
||||
<LenticularCard v-if="lenticularLayers.length > 0" class="craft-lenticular-card"
|
||||
:layers="lenticularLayers" :transforms="layerTransforms"
|
||||
:gyro-source="gyroSourceLabel" :skip-built-in-touch="true" tilt-hint-text="晃动查看"
|
||||
gyro-source="simulation" tilt-hint-text="晃动查看"
|
||||
:shimmer-mid-opacity="0.16" @simulate="simulate" />
|
||||
</view>
|
||||
</view>
|
||||
@ -107,9 +107,6 @@ const lenticularLayers = ref([])
|
||||
const {
|
||||
layerTransforms,
|
||||
simulate,
|
||||
gyroSourceLabel,
|
||||
scheduleTiltStart,
|
||||
stopTiltPreview,
|
||||
} = useLenticularCraftTiltPreview(lenticularLayers);
|
||||
|
||||
// NftCard 样式
|
||||
@ -190,7 +187,6 @@ onMounted(async () => {
|
||||
if ((asset.is_lenticular || data.is_lenticular) && bgImageUrl && imageUrl) {
|
||||
isLenticular.value = true
|
||||
lenticularLayers.value = buildLenticularLayersTwo(bgImageUrl, imageUrl)
|
||||
scheduleTiltStart()
|
||||
}
|
||||
} else {
|
||||
// API 返回异常,使用本地数据
|
||||
@ -239,7 +235,6 @@ function applyLocalNftData(data) {
|
||||
if (data.is_lenticular && data.image && data.bg_image) {
|
||||
isLenticular.value = true
|
||||
lenticularLayers.value = buildLenticularLayersTwo(data.bg_image, data.image)
|
||||
scheduleTiltStart()
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +267,6 @@ const handleViewDetails = () => {
|
||||
};
|
||||
|
||||
onUnload(() => {
|
||||
stopTiltPreview()
|
||||
try {
|
||||
uni.removeStorageSync('temp_nft_data');
|
||||
} catch (e) {
|
||||
|
||||
@ -33,8 +33,7 @@
|
||||
class="lenticular-preview"
|
||||
:layers="lenticularLayers"
|
||||
:transforms="layerTransforms"
|
||||
:gyro-source="gyroSourceLabel"
|
||||
:skip-built-in-touch="true"
|
||||
gyro-source="simulation"
|
||||
tilt-hint-text="晃动查看"
|
||||
:shimmer-mid-opacity="0.16"
|
||||
@simulate="simulate"
|
||||
@ -91,8 +90,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow, onHide } from '@dcloudio/uni-app';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
|
||||
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
|
||||
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
|
||||
@ -125,9 +123,6 @@ const lenticularLayers = ref([]);
|
||||
const {
|
||||
layerTransforms,
|
||||
simulate,
|
||||
gyroSourceLabel,
|
||||
scheduleTiltStart,
|
||||
stopTiltPreview,
|
||||
} = useLenticularCraftTiltPreview(lenticularLayers);
|
||||
|
||||
const isCraftDetailFlow = computed(() => isDetailAfterSelect(craftFormData.value));
|
||||
@ -802,7 +797,6 @@ const initLenticularPreview = () => {
|
||||
const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
lenticularLayers.value = buildLenticularLayersTwo(p.bgPath || '', p.subjectPath || '');
|
||||
selectedIndex.value = 0;
|
||||
scheduleTiltStart();
|
||||
} catch (e) {
|
||||
console.error('[GenerationResult] lenticular init', e);
|
||||
}
|
||||
@ -849,19 +843,6 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (isLenticularDisplay.value && lenticularLayers.value.length) {
|
||||
scheduleTiltStart();
|
||||
}
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
stopTiltPreview();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTiltPreview();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user