/** * 铸爱光栅预览(lenticular-result / asset-detail 等):物理参数 + 陀螺仪倾斜 → 离散档位映射 * * 引擎是连续叠化引擎(gamma -1..+1 → 层组叠化),但上层做硬切: * 1. 陀螺仪传感器 → filtered (dx, dy) 2D 向量 * 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' const FLOW_PHYSICS = { angleStability: 15, // 显示层跟踪速度(0=最快/最跟手;100=最稳/最惰性) transitionSmoothness: 2, // 输入层跟踪速度(值越小越瞬时;引擎内部: alpha=1-s*0.9) tiltSensitivity: 96, // gamma 放大倍率(g *= 0.44+sensi*0.52) sensorDeadzoneStrength: 1, // 零点死区强度 parallaxDepth: 18, lenticularAnchorFloor: 0.06, lenticularNonDominantResidualMin: 0.04, lenticularPrevLayerGhostMin: 0.04, lenticularBlendBaseScale: 0.5, } // 离散档位阈值(度) // 幅值 > ACTIVATE → 触发硬切;幅值 < DEACTIVATE → 回中间 // ACTIVATE 放宽到 7° 以减少振荡 const TILT_ACTIVATE_DEG = 7 const TILT_DEACTIVATE_DEG = 2.5 // 方向符号滞回:连续 N 帧相反方向才翻转(杜绝抖动) const DIR_HYST_FRAMES = 8 /** 进入页面后的传感器启动延迟(ms) */ export const LENTICULAR_TILT_START_DELAY_MS = 260 const DEBUG_TILT_PREVIEW_LOG = true const dlog = (...args) => { if (DEBUG_TILT_PREVIEW_LOG) console.log(...args) } /** * @param {import('vue').Ref|import('vue').Ref} layersRef */ export function useLenticularCraftTiltPreview(layersRef) { const gyroSourceLabel = ref('simulation') const { physics, layerTransforms, simulate, relax, snapSimulatedTilt } = useLenticularPreview(layersRef) Object.assign(physics, FLOW_PHYSICS) /** 上一帧离散档位,-1=未初始化 */ let lastLayerIdx = -1 /** 硬切冷却时间戳:上次 snap 的 now(),在冷却期内不响应 */ let snapCooldownUntil = 0 /** 硬切冷却时长(ms),防止传感器噪声导致高频振荡 */ const SNAP_COOLDOWN_MS = 800 /** 方向符号滞回状态:当前方向(-1=左, 0=未定, 1=右) */ let dirSign = 0 let dirOppositeStreak = 0 function getLayersArray() { const v = layersRef.value !== undefined ? layersRef.value : layersRef return Array.isArray(v) ? v : [] } /** * (dx, dy) = 快慢通道差值(度)→ 离散层索引 → gamma ±1/0 * - 方向仅取 dx(光栅卡左右倾斜),用独立符号滞回 * - dy 参与幅值计算但不影响方向决策 * - 传感器调用此函数,输出到引擎并重置层平滑 */ function simulateFromSignedDegrees(dx, dy) { const layers = getLayersArray() const count = layers.length if (count < 2) { simulate(0, 0) return } const tiltMag = Math.sqrt(dx * dx + dy * dy) // 幅值低于归中阈值 → 回中间 if (tiltMag < TILT_DEACTIVATE_DEG) { if (count === 2) { // 2 层卡无中间态,初始不 snap,有档位则保持 if (lastLayerIdx < 0) return return } // ≥3 层卡回中间 const centerIdx = Math.floor(count / 2) if (lastLayerIdx === centerIdx) return lastLayerIdx = centerIdx dirSign = 0 dirOppositeStreak = 0 const now = Date.now() if (now < snapCooldownUntil) return snapCooldownUntil = now + SNAP_COOLDOWN_MS dlog('[Discrete] mag:', tiltMag.toFixed(1), '→ center') simulate(0, 0) snapSimulatedTilt({ resetLayerSmoothing: true }) return } // 幅值超激活阈值 → 判断方向 if (tiltMag < TILT_ACTIVATE_DEG) { // 滞回区:保持上次;2 层卡无中间态,初始不锁 lastLayerIdx if (count === 2) { if (lastLayerIdx < 0) return return } if (lastLayerIdx >= 0) return // 首次且滞回区 → 居中(仅 ≥3 层卡) lastLayerIdx = Math.floor(count / 2) return } // 方向:仅用 x 轴(光栅卡左右倾斜),带独立符号滞回 const rawDir = dx > 0 ? 1 : -1 if (dirSign !== 0 && rawDir !== dirSign) { dirOppositeStreak++ if (dirOppositeStreak < DIR_HYST_FRAMES) { // 方向尚未确认翻转 → 沿用旧方向 // 但 targetIdx 不变,无需处理(下一段会算出相同的 targetIdx) } else { // 方向翻转确认 dirSign = rawDir dirOppositeStreak = 0 } } else { dirOppositeStreak = 0 dirSign = rawDir } const targetIdx = dirSign < 0 ? 0 : (count - 1) if (targetIdx === lastLayerIdx) return lastLayerIdx = targetIdx // 冷却检查 const now = Date.now() if (now < snapCooldownUntil) return snapCooldownUntil = now + SNAP_COOLDOWN_MS // layerIdx → gamma let gamma if (count === 2) { gamma = targetIdx === 0 ? -1 : 1 } else if (targetIdx <= 0) { gamma = -1 } else if (targetIdx >= count - 1) { gamma = 1 } else { gamma = 0 } dlog('[Discrete] mag:', tiltMag.toFixed(1), 'dx:', dx.toFixed(1), '→ idx:', targetIdx, 'gamma:', gamma) simulate(gamma, 0) snapSimulatedTilt({ resetLayerSmoothing: true }) } /** 归零预览:无论当前在哪个位置,平滑回到中间 */ function lockPreviewStill() { lastLayerIdx = -1 snapCooldownUntil = 0 dirSign = 0 dirOppositeStreak = 0 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() } return { physics, layerTransforms, simulate, relax, gyroSourceLabel, scheduleTiltStart, stopTiltPreview, lockPreviewStill, } }