156 lines
3.5 KiB
JavaScript
156 lines
3.5 KiB
JavaScript
/**
|
||
* 光栅卡预览:触摸模拟倾斜 + LenticularEngine(不启动硬件传感器)
|
||
* @param {import('vue').Ref<Array>} layersRef 图层配置(reactive/ref 均可被 .value 读取时用 ref)
|
||
*/
|
||
import { reactive, ref, watch, onMounted, onUnmounted } from 'vue'
|
||
import { LenticularEngine, DEFAULT_PHYSICS } from '@/utils/lenticular-engine.js'
|
||
|
||
function clamp(v, min, max) {
|
||
return Math.max(min, Math.min(max, v))
|
||
}
|
||
|
||
export function useLenticularPreview(layersRef) {
|
||
const physics = reactive({ ...DEFAULT_PHYSICS })
|
||
physics.gyroSimEnabled = false
|
||
|
||
const sensorData = ref({ gamma: 0, beta: 0, timestamp: Date.now() })
|
||
const source = ref('simulation')
|
||
|
||
function simulate(x, _y) {
|
||
sensorData.value = {
|
||
gamma: clamp(x, -1, 1),
|
||
beta: 0,
|
||
timestamp: Date.now(),
|
||
}
|
||
}
|
||
|
||
function relax(factor = 0.85) {
|
||
sensorData.value = {
|
||
gamma: sensorData.value.gamma * factor,
|
||
beta: 0,
|
||
timestamp: Date.now(),
|
||
}
|
||
}
|
||
|
||
const engine = new LenticularEngine(physics)
|
||
const initial = layersRef.value || layersRef
|
||
engine.setLayers(Array.isArray(initial) ? initial : [])
|
||
|
||
const layerTransforms = ref({})
|
||
|
||
function rebuildTransformsFromLayers(ls) {
|
||
const prev = layerTransforms.value
|
||
const next = {}
|
||
for (const l of ls) {
|
||
const p = prev[l.id]
|
||
next[l.id] =
|
||
p != null ? p : { x: 0, y: 0, opacity: l.opacity }
|
||
}
|
||
layerTransforms.value = next
|
||
}
|
||
|
||
rebuildTransformsFromLayers(engine.layers.length ? engine.layers : layersRef.value || [])
|
||
|
||
const stripeRender = ref({
|
||
phaseShift: 0,
|
||
shares: [1 / 3, 1 / 3, 1 / 3],
|
||
pitchPx: DEFAULT_PHYSICS.lenticularPitchPx,
|
||
prevLayerGhost: null,
|
||
})
|
||
|
||
let rafId = null
|
||
const nextFrame =
|
||
typeof requestAnimationFrame === 'function'
|
||
? (cb) => requestAnimationFrame(cb)
|
||
: (cb) => setTimeout(cb, 16)
|
||
const cancelFrame =
|
||
typeof cancelAnimationFrame === 'function'
|
||
? (id) => cancelAnimationFrame(id)
|
||
: (id) => clearTimeout(id)
|
||
|
||
function getLayersArray() {
|
||
const v = layersRef.value !== undefined ? layersRef.value : layersRef
|
||
return Array.isArray(v) ? v : []
|
||
}
|
||
|
||
function tick() {
|
||
try {
|
||
const ls = getLayersArray()
|
||
const renderState = engine.feedSimulatedTilt(sensorData.value.gamma, sensorData.value.beta)
|
||
const next = {}
|
||
for (const layer of ls) {
|
||
const offset = renderState.layerOffsets.get(layer.id)
|
||
const opacity = renderState.layerOpacities.get(layer.id)
|
||
next[layer.id] = {
|
||
x: offset != null ? offset.x : 0,
|
||
y: offset != null ? offset.y : 0,
|
||
opacity: opacity != null ? opacity : layer.opacity,
|
||
}
|
||
}
|
||
layerTransforms.value = next
|
||
stripeRender.value = {
|
||
phaseShift: renderState.stripePhaseShift,
|
||
shares: [...renderState.stripShares],
|
||
pitchPx: renderState.lenticularPitchPx,
|
||
prevLayerGhost: renderState.prevLayerGhost,
|
||
}
|
||
} catch (e) {
|
||
console.error('[useLenticularPreview] tick failed', e)
|
||
}
|
||
rafId = nextFrame(tick)
|
||
}
|
||
|
||
function startRenderLoop() {
|
||
if (rafId != null) return
|
||
rafId = nextFrame(tick)
|
||
}
|
||
|
||
function stopRenderLoop() {
|
||
if (rafId != null) {
|
||
cancelFrame(rafId)
|
||
rafId = null
|
||
}
|
||
}
|
||
|
||
watch(
|
||
layersRef,
|
||
(ls) => {
|
||
const arr = Array.isArray(ls) ? ls : []
|
||
engine.setLayers(arr)
|
||
rebuildTransformsFromLayers(arr)
|
||
},
|
||
{ deep: true, immediate: true }
|
||
)
|
||
|
||
onMounted(() => {
|
||
startRenderLoop()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopRenderLoop()
|
||
})
|
||
|
||
const gyro = {
|
||
sensorData,
|
||
source,
|
||
simulate,
|
||
relax,
|
||
start: () => {},
|
||
stop: () => {
|
||
source.value = 'simulation'
|
||
},
|
||
}
|
||
|
||
return {
|
||
physics,
|
||
layerTransforms,
|
||
stripeRender,
|
||
gyro,
|
||
simulate,
|
||
relax,
|
||
engine,
|
||
startRenderLoop,
|
||
stopRenderLoop,
|
||
}
|
||
}
|