topfans/frontend/pages/square/composables/useSwipe.js
2026-04-28 16:05:55 +08:00

182 lines
4.2 KiB
JavaScript

import { ref, computed } from 'vue'
// RAF 封装
const rafFn = (cb) => uni.requestAnimationFrame ? uni.requestAnimationFrame(cb) : setTimeout(cb, 16)
const cafFn = (id) => uni.cancelAnimationFrame ? uni.cancelAnimationFrame(id) : clearTimeout(id)
export function useSwipe() {
const bgOffsetX = ref(0)
const screenWidth = ref(375)
const tileWidth = ref(375)
let rawOffsetX = 0
let touchStartX = 0
let lastMoveX = 0
let lastMoveTime = 0
let velocity = 0
let inertiaRaf = null
let isInertiaPhase = false
let touchInBanner = false
let onTileChange = null
const cabinLayerStyle = computed(() => ({
transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`
}))
const backgroundStripStyle = computed(() => ({
width: `${tileWidth.value * 3}px`,
transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`
}))
const clampOffset = (offset) => {
const w = tileWidth.value
return (((offset % w) + w) % w) - w
}
const normalizeOffset = (offset) => {
const w = tileWidth.value
const normalized = clampOffset(offset)
const prevTileN = Math.floor(-rawOffsetX / w)
rawOffsetX += offset - bgOffsetX.value
const nextTileN = Math.floor(-rawOffsetX / w)
const delta = nextTileN - prevTileN
if (delta !== 0 && onTileChange) {
onTileChange(delta, isInertiaPhase)
}
return normalized
}
const stopInertia = () => {
if (inertiaRaf) {
cafFn(inertiaRaf)
inertiaRaf = null
}
isInertiaPhase = false
}
const scrollPage = (direction) => {
stopInertia()
if (onTileChange) {
onTileChange(direction, false)
}
const DURATION = 300
const FRAME = 16
const totalFrames = Math.round(DURATION / FRAME)
const totalDelta = tileWidth.value * direction * -1
let frame = 0
const step = () => {
frame++
const progress = frame / totalFrames
const eased = 1 - Math.pow(1 - progress, 3)
const prevEased = frame === 1 ? 0 : 1 - Math.pow(1 - (frame - 1) / totalFrames, 3)
const delta = totalDelta * (eased - prevEased)
bgOffsetX.value = clampOffset(bgOffsetX.value + delta)
rawOffsetX += delta
if (frame < totalFrames) {
inertiaRaf = rafFn(step)
}
}
inertiaRaf = rafFn(step)
}
const getBannerBottom = () => (screenWidth.value / 750) * 632
const onBgTouchStart = (e) => {
const touchY = e.touches[0].clientY
touchInBanner = touchY < getBannerBottom()
if (touchInBanner) return
stopInertia()
touchStartX = e.touches[0].clientX
lastMoveX = touchStartX
lastMoveTime = Date.now()
velocity = 0
}
const onBgTouchMove = (e) => {
if (touchInBanner) return
e.preventDefault()
const currentX = e.touches[0].clientX
const now = Date.now()
const dt = now - lastMoveTime || 1
velocity = (currentX - lastMoveX) / dt
lastMoveX = currentX
lastMoveTime = now
bgOffsetX.value = normalizeOffset(bgOffsetX.value + (currentX - touchStartX))
touchStartX = currentX
}
const onBgTouchEnd = () => {
if (touchInBanner) {
touchInBanner = false
return
}
touchInBanner = false
isInertiaPhase = true
const FRICTION = 0.8
const MIN_VELOCITY = 0.2
const step = () => {
velocity *= FRICTION
if (Math.abs(velocity) < MIN_VELOCITY) {
isInertiaPhase = false
return
}
bgOffsetX.value = normalizeOffset(bgOffsetX.value + velocity * 16)
inertiaRaf = rafFn(step)
}
inertiaRaf = rafFn(step)
}
const onBgTouchCancel = () => {
touchInBanner = false
stopInertia()
velocity = 0
}
const initSwipe = ({ screenW, tileW, onTileChangeCallback }) => {
screenWidth.value = screenW
tileWidth.value = tileW
onTileChange = onTileChangeCallback
bgOffsetX.value = 0
rawOffsetX = 0
velocity = 0
}
const reset = () => {
stopInertia()
bgOffsetX.value = 0
rawOffsetX = 0
velocity = 0
}
return {
bgOffsetX,
rawOffsetX,
velocity,
cabinLayerStyle,
backgroundStripStyle,
scrollPage,
stopInertia,
initSwipe,
reset,
onBgTouchStart,
onBgTouchMove,
onBgTouchEnd,
onBgTouchCancel,
}
}