226 lines
6.4 KiB
JavaScript
226 lines
6.4 KiB
JavaScript
/**
|
||
* 铸爱光栅预览(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,
|
||
}
|
||
}
|