219 lines
6.5 KiB
JavaScript
219 lines
6.5 KiB
JavaScript
/**
|
||
* 铸爱光栅预览(lenticular-result / asset-detail 等):手动倾斜入口 → 离散档位映射
|
||
*
|
||
* 引擎是连续叠化引擎(gamma -1..+1 → 层组叠化),但上层做硬切:
|
||
* 1. 手动入口(滑块 / 触摸)→ 归一化 -1..+1 → 度数
|
||
* 2. (dx, dy) → 2D 幅值 + 方向 → 离散 layerIdx(硬阈值 + 滞回 + 冷却)
|
||
* 3. layerIdx → gamma ±1/0 → engine + snapSimulatedTilt 重置平滑
|
||
*/
|
||
import { useLenticularPreview } from '@/composables/useLenticularPreview.js'
|
||
|
||
// 手动模拟倾斜:归一化 → 度数 转换系数(spec §3.5)
|
||
const NORMALIZED_TO_DEG = 45
|
||
|
||
const FLOW_PHYSICS = {
|
||
angleStability: 5, // 显示层跟踪速度(0=最快/最跟手;100=最稳/最惰性),调小消除 displayGamma 滞后感
|
||
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: 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 { 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: false })
|
||
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)
|
||
}
|
||
|
||
// ====================================================================
|
||
// 手动模拟倾斜入口(spec §3.1)
|
||
// ====================================================================
|
||
|
||
/**
|
||
* 滑块/外部入口:直接传度数(-45..+45)
|
||
* 内部转给 simulateFromSignedDegrees 走现有离散档位逻辑
|
||
*/
|
||
function simulateTilt(dx, dy) {
|
||
if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
|
||
console.warn('[CraftTilt] simulateTilt: non-finite input', dx, dy)
|
||
return
|
||
}
|
||
simulateFromSignedDegrees(dx, dy)
|
||
}
|
||
|
||
/**
|
||
* 触摸入口:-1..+1 归一化转度数(系数 45)
|
||
* 由 LenticularCard.vue 的 @simulate 事件触发
|
||
*/
|
||
function simulateTiltFromNormalized(x, y) {
|
||
const dx = x * NORMALIZED_TO_DEG
|
||
const dy = (y != null ? y : 0) * NORMALIZED_TO_DEG
|
||
simulateTilt(dx, dy)
|
||
}
|
||
|
||
/**
|
||
* 滑块点"复位"时调用:归零 + 清空离散档位状态
|
||
*/
|
||
function simulateTiltReset() {
|
||
lockPreviewStill()
|
||
}
|
||
|
||
return {
|
||
physics,
|
||
layerTransforms,
|
||
simulate,
|
||
relax,
|
||
lockPreviewStill,
|
||
// 新增:手动模拟入口(spec §3.1)
|
||
simulateTilt,
|
||
simulateTiltFromNormalized,
|
||
simulateTiltReset,
|
||
}
|
||
}
|