PPTist/src/views/Screen/hooks/useExecPlay.ts

235 lines
7.5 KiB
TypeScript

import { onMounted, onUnmounted, ref } from 'vue'
import { throttle } from 'lodash'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { KEYS } from '@/configs/hotkey'
import { ANIMATION_CLASS_PREFIX } from '@/configs/animation'
import { message } from 'ant-design-vue'
export default () => {
const slidesStore = useSlidesStore()
const { slides, slideIndex, formatedAnimations } = storeToRefs(slidesStore)
// 当前页的元素动画执行到的位置
const animationIndex = ref(0)
// 动画执行状态
const inAnimation = ref(false)
// 最小已播放页面索引
const playedSlidesMinIndex = ref(slideIndex.value)
// 执行元素动画
const runAnimation = () => {
// 正在执行动画时,禁止其他新的动画开始
if (inAnimation.value) return
const { animations, autoNext } = formatedAnimations.value[animationIndex.value]
animationIndex.value += 1
// 标记开始执行动画
inAnimation.value = true
let endAnimationCount = 0
// 依次执行该位置中的全部动画
for (const animation of animations) {
const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (!elRef) {
endAnimationCount += 1
continue
}
const animationName = `${ANIMATION_CLASS_PREFIX}${animation.effect}`
// 执行动画前先清除原有的动画状态(如果有)
elRef.style.removeProperty('--animate-duration')
for (const classname of elRef.classList) {
if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`)
}
// 执行动画
elRef.style.setProperty('--animate-duration', `${animation.duration}ms`)
elRef.classList.add(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
// 执行动画结束,将“退场”以外的动画状态清除
const handleAnimationEnd = () => {
if (animation.type !== 'out') {
elRef.style.removeProperty('--animate-duration')
elRef.classList.remove(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
}
// 判断该位置上的全部动画都已经结束后,标记动画执行完成,并尝试继续向下执行(如果有需要)
endAnimationCount += 1
if (endAnimationCount === animations.length) {
inAnimation.value = false
if (autoNext) runAnimation()
}
}
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
}
}
// 撤销元素动画,除了将索引前移外,还需要清除动画状态
const revokeAnimation = () => {
animationIndex.value -= 1
const { animations } = formatedAnimations.value[animationIndex.value]
for (const animation of animations) {
const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (!elRef) continue
elRef.style.removeProperty('--animate-duration')
for (const classname of elRef.classList) {
if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`)
}
}
// 如果撤销时该位置有且仅有强调动画,则继续执行一次撤销
if (animations.every(item => item.type === 'attention')) execPrev()
}
// 关闭自动播放
const autoPlayTimer = ref(0)
const closeAutoPlay = () => {
if (autoPlayTimer.value) {
clearInterval(autoPlayTimer.value)
autoPlayTimer.value = 0
}
}
onUnmounted(closeAutoPlay)
const throttleMassage = throttle(function(msg) {
message.success(msg)
}, 1000, { leading: true, trailing: false })
// 向上/向下播放
// 遇到元素动画时,优先执行动画播放,无动画则执行翻页
// 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画
// 撤回到上一页时,若该页从未播放过(意味着不存在动画状态),需要将动画索引置为最小值(初始状态),否则置为最大值(最终状态)
const execPrev = () => {
if (formatedAnimations.value.length && animationIndex.value > 0) {
revokeAnimation()
}
else if (slideIndex.value > 0) {
slidesStore.updateSlideIndex(slideIndex.value - 1)
if (slideIndex.value < playedSlidesMinIndex.value) {
animationIndex.value = 0
playedSlidesMinIndex.value = slideIndex.value
}
else animationIndex.value = formatedAnimations.value.length
inAnimation.value = false
}
else {
throttleMassage('已经是第一页了')
inAnimation.value = false
}
}
const execNext = () => {
if (formatedAnimations.value.length && animationIndex.value < formatedAnimations.value.length) {
runAnimation()
}
else if (slideIndex.value < slides.value.length - 1) {
slidesStore.updateSlideIndex(slideIndex.value + 1)
animationIndex.value = 0
inAnimation.value = false
}
else {
throttleMassage('已经是最后一页了')
closeAutoPlay()
inAnimation.value = false
}
}
// 自动播放
const autoPlay = () => {
closeAutoPlay()
message.success('开始自动放映')
autoPlayTimer.value = setInterval(execNext, 2500)
}
// 鼠标滚动翻页
const mousewheelListener = throttle(function(e: WheelEvent) {
if (e.deltaY < 0) execPrev()
else if (e.deltaY > 0) execNext()
}, 500, { leading: true, trailing: false })
// 触摸屏上下滑动翻页
const touchInfo = ref<{ x: number; y: number; } | null>(null)
const touchStartListener = (e: TouchEvent) => {
touchInfo.value = {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY,
}
}
const touchEndListener = (e: TouchEvent) => {
if (!touchInfo.value) return
const offsetX = Math.abs(touchInfo.value.x - e.changedTouches[0].pageX)
const offsetY = e.changedTouches[0].pageY - touchInfo.value.y
if ( Math.abs(offsetY) > offsetX && Math.abs(offsetY) > 50 ) {
touchInfo.value = null
if (offsetY > 0) execPrev()
else execNext()
}
}
// 快捷键翻页
const keydownListener = (e: KeyboardEvent) => {
const key = e.key.toUpperCase()
if (key === KEYS.UP || key === KEYS.LEFT) execPrev()
else if (
key === KEYS.DOWN ||
key === KEYS.RIGHT ||
key === KEYS.SPACE ||
key === KEYS.ENTER
) execNext()
}
onMounted(() => document.addEventListener('keydown', keydownListener))
onUnmounted(() => document.removeEventListener('keydown', keydownListener))
// 切换到上一张/上一张幻灯片(无视元素的入场动画)
const turnPrevSlide = () => {
slidesStore.updateSlideIndex(slideIndex.value - 1)
animationIndex.value = 0
}
const turnNextSlide = () => {
slidesStore.updateSlideIndex(slideIndex.value + 1)
animationIndex.value = 0
}
// 切换幻灯片到指定的页面
const turnSlideToIndex = (index: number) => {
slidesStore.updateSlideIndex(index)
animationIndex.value = 0
}
const turnSlideToId = (id: string) => {
const index = slides.value.findIndex(slide => slide.id === id)
if (index !== -1) {
slidesStore.updateSlideIndex(index)
animationIndex.value = 0
}
}
return {
autoPlayTimer,
autoPlay,
closeAutoPlay,
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
execPrev,
execNext,
animationIndex,
}
}