topfans/frontend/composables/useLenticularCraftTiltPreview.js
2026-06-03 22:19:22 +08:00

226 lines
6.4 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.

/**
* 铸爱光栅预览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<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)
/** 上一帧离散档位,-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,
}
}