topfans/frontend/composables/useLenticularCraftTiltPreview.js
2026-07-09 16:08:25 +08:00

219 lines
6.5 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. 手动入口(滑块 / 触摸)→ 归一化 -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,
}
}