topfans/frontend/composables/useLenticularCraftTiltPreview.js
2026-05-16 02:42:32 +08:00

164 lines
4.2 KiB
JavaScript

/**
* 铸爱流程中的光栅预览:与 lenticular-studio 一致的物理参数、离散档位与陀螺仪启动节奏
*/
import { ref, nextTick } from 'vue'
import { useLenticularPreview } from '@/composables/useLenticularPreview.js'
import { useLenticularStudioTilt } from '@/composables/useLenticularStudioTilt.js'
const FLOW_PHYSICS = {
angleStability: 52,
transitionSmoothness: 40,
tiltSensitivity: 96,
sensorDeadzoneStrength: 0,
parallaxDepth: 18,
lenticularAnchorFloor: 0.1,
lenticularNonDominantResidualMin: 0.092,
lenticularPrevLayerGhostMin: 0.098,
lenticularBlendBaseScale: 1.1,
}
const DISCRETE_STEP_DEG = 13
const DISCRETE_STEP_HYST_DEG = 5
const TILT_MAG_TAU_MS = 150
const TILT_MAG_MAX_RISE_DPS = 82
const TILT_MAG_MAX_FALL_DPS = 168
const TILT_AWAY_FROM_LEVEL_DPS = 72
const TILT_RISE_TIGHTEN = 0.42
export const LENTICULAR_TILT_START_DELAY_MS = 560
/**
* @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)
const discreteStableStep = ref(0)
let tiltMagEma = 0
let lastTiltMagSampleAt = 0
let lastSignedDegHint = null
function resetTiltMagFilter() {
tiltMagEma = 0
lastTiltMagSampleAt = 0
lastSignedDegHint = null
}
function getLayersArray() {
const v = layersRef.value !== undefined ? layersRef.value : layersRef
return Array.isArray(v) ? v : []
}
function simulateFromSignedDegrees(tiltMagDeg, signedDegHint) {
const ls = getLayersArray()
const n = Math.max(1, ls.length)
const raw = Math.abs(Number(tiltMagDeg) || 0)
const now = Date.now()
let dt = 33
if (lastTiltMagSampleAt > 0) {
dt = now - lastTiltMagSampleAt
}
lastTiltMagSampleAt = now
dt = Math.max(16, Math.min(100, dt))
const alpha = 1 - Math.exp(-dt / TILT_MAG_TAU_MS)
let nextEma = tiltMagEma + (raw - tiltMagEma) * alpha
const sec = dt * 0.001
let maxRise = TILT_MAG_MAX_RISE_DPS * sec
const maxFall = TILT_MAG_MAX_FALL_DPS * sec
if (Number.isFinite(signedDegHint)) {
if (lastSignedDegHint != null && sec > 1e-6) {
const dAbsSignedDt =
(Math.abs(signedDegHint) - Math.abs(lastSignedDegHint)) / sec
if (dAbsSignedDt > TILT_AWAY_FROM_LEVEL_DPS) {
maxRise *= TILT_RISE_TIGHTEN
}
}
lastSignedDegHint = signedDegHint
} else {
lastSignedDegHint = null
}
let dMag = nextEma - tiltMagEma
if (dMag > 0) {
dMag = Math.min(dMag, maxRise)
} else {
dMag = Math.max(dMag, -maxFall)
}
tiltMagEma += dMag
const absDeg = tiltMagEma
const STEP = DISCRETE_STEP_DEG
const MARGIN = DISCRETE_STEP_HYST_DEG
const stepBefore = discreteStableStep.value
let s = stepBefore
while (absDeg >= (s + 1) * STEP + MARGIN) s++
while (s > 0 && absDeg <= s * STEP - MARGIN) s--
discreteStableStep.value = s
const idx = s % n
const sens = physics.tiltSensitivity / 100
const mul = 0.44 + sens * 0.52
const u = (idx + 0.5) / n
const gPick = Math.max(-1, Math.min(1, 2 * u - 1))
const gamma = Math.max(-1, Math.min(1, gPick / mul))
simulate(gamma, 0)
}
function lockPreviewStill() {
discreteStableStep.value = 0
resetTiltMagFilter()
simulateFromSignedDegrees(0)
snapSimulatedTilt({ resetLayerSmoothing: true })
}
const { start: startTilt, stop: stopTilt } = useLenticularStudioTilt({
simulate,
simulateFromSignedDegrees,
gyroSourceLabel,
useStudioAccelDirect: true,
onTiltDriverFallback: () => {
discreteStableStep.value = 0
resetTiltMagFilter()
},
})
let entryTiltTimer = null
function cancelScheduledTiltStart() {
if (entryTiltTimer != null) {
clearTimeout(entryTiltTimer)
entryTiltTimer = null
}
}
function scheduleTiltStart() {
cancelScheduledTiltStart()
nextTick(() => {
relax(0.86)
lockPreviewStill()
entryTiltTimer = setTimeout(() => {
entryTiltTimer = null
startTilt()
}, LENTICULAR_TILT_START_DELAY_MS)
})
}
function stopTiltPreview() {
cancelScheduledTiltStart()
stopTilt()
}
return {
physics,
layerTransforms,
simulate,
relax,
gyroSourceLabel,
scheduleTiltStart,
stopTiltPreview,
lockPreviewStill,
}
}