feat: 支持元素多动画,动画启动方式,延迟,退场动画

This commit is contained in:
ChaoXxxx 2022-05-09 11:11:47 +08:00
parent ff03c2acd2
commit b5383c45fe
8 changed files with 516 additions and 126 deletions

View File

@ -9,6 +9,7 @@ export const ANIMATIONS = [
{ name: '向上弹入', value: 'bounceInUp' }, { name: '向上弹入', value: 'bounceInUp' },
{ name: '向下弹入', value: 'bounceInDown' }, { name: '向下弹入', value: 'bounceInDown' },
], ],
hiddenElement: []
}, },
{ {
type: 'fade', type: 'fade',
@ -28,6 +29,7 @@ export const ANIMATIONS = [
{ name: '从左下浮入', value: 'fadeInBottomLeft' }, { name: '从左下浮入', value: 'fadeInBottomLeft' },
{ name: '从右下浮入', value: 'fadeInBottomRight' }, { name: '从右下浮入', value: 'fadeInBottomRight' },
], ],
hiddenElement: []
}, },
{ {
type: 'rotate', type: 'rotate',
@ -39,6 +41,7 @@ export const ANIMATIONS = [
{ name: '绕左上旋转进入', value: 'rotateInUpLeft' }, { name: '绕左上旋转进入', value: 'rotateInUpLeft' },
{ name: '绕右上旋转进入', value: 'rotateInUpRight' }, { name: '绕右上旋转进入', value: 'rotateInUpRight' },
], ],
hiddenElement: []
}, },
{ {
type: 'zoom', type: 'zoom',
@ -50,6 +53,7 @@ export const ANIMATIONS = [
{ name: '从右放大进入', value: 'zoomInRight' }, { name: '从右放大进入', value: 'zoomInRight' },
{ name: '向上放大进入', value: 'zoomInUp' }, { name: '向上放大进入', value: 'zoomInUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'slide', type: 'slide',
@ -60,6 +64,7 @@ export const ANIMATIONS = [
{ name: '从左滑入', value: 'slideInRight' }, { name: '从左滑入', value: 'slideInRight' },
{ name: '向上滑入', value: 'slideInUp' }, { name: '向上滑入', value: 'slideInUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'flip', type: 'flip',
@ -68,6 +73,7 @@ export const ANIMATIONS = [
{ name: 'X轴翻转进入', value: 'flipInX' }, { name: 'X轴翻转进入', value: 'flipInX' },
{ name: 'Y轴翻转进入', value: 'flipInY' }, { name: 'Y轴翻转进入', value: 'flipInY' },
], ],
hiddenElement: []
}, },
{ {
type: 'back', type: 'back',
@ -78,6 +84,7 @@ export const ANIMATIONS = [
{ name: '从右放大滑入', value: 'backInRight' }, { name: '从右放大滑入', value: 'backInRight' },
{ name: '向上放大滑入', value: 'backInUp' }, { name: '向上放大滑入', value: 'backInUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'lightSpeed', type: 'lightSpeed',
@ -86,5 +93,105 @@ export const ANIMATIONS = [
{ name: '从右飞入', value: 'lightSpeedInRight' }, { name: '从右飞入', value: 'lightSpeedInRight' },
{ name: '从左飞入', value: 'lightSpeedInLeft' }, { name: '从左飞入', value: 'lightSpeedInLeft' },
], ],
hiddenElement: []
},
]
export const ANIMATIONS_EXITS = [
{
type: 'bounce',
name: '弹跳',
children: [
{ name: '弹出', value: 'bounceOut' },
{ name: '向左弹出', value: 'bounceOutLeft' },
{ name: '向右弹出', value: 'bounceOutRight' },
{ name: '向上弹出', value: 'bounceOutUp' },
{ name: '向下弹出', value: 'bounceOutDown' },
],
hiddenElement: []
},
{
type: 'fade',
name: '浮现',
children: [
{ name: '浮出', value: 'fadeOut' },
{ name: '向下浮出', value: 'fadeOutDown' },
{ name: '向下长距浮出', value: 'fadeOutDownBig' },
{ name: '向左浮出', value: 'fadeOutLeft' },
{ name: '向左长距浮出', value: 'fadeOutLeftBig' },
{ name: '向右浮出', value: 'fadeOutRight' },
{ name: '向右长距浮出', value: 'fadeOutRightBig' },
{ name: '向上浮出', value: 'fadeOutUp' },
{ name: '向上长距浮出', value: 'fadeOutUpBig' },
{ name: '从左上浮出', value: 'fadeOutTopLeft' },
{ name: '从右上浮出', value: 'fadeOutTopRight' },
{ name: '从左下浮出', value: 'fadeOutBottomLeft' },
{ name: '从右下浮出', value: 'fadeOutBottomRight' },
],
hiddenElement: []
},
{
type: 'rotate',
name: '旋转',
children: [
{ name: '旋转退出', value: 'rotateOut' },
{ name: '绕左下旋转退出', value: 'rotateOutDownLeft' },
{ name: '绕右下旋转退出', value: 'rotateOutDownRight' },
{ name: '绕左上旋转退出', value: 'rotateOutUpLeft' },
{ name: '绕右上旋转退出', value: 'rotateOutUpRight' },
],
hiddenElement: []
},
{
type: 'zoom',
name: '缩放',
children: [
{ name: '缩小退出', value: 'zoomOut' },
{ name: '向下缩小退出', value: 'zoomOutDown' },
{ name: '从左缩小退出', value: 'zoomOutLeft' },
{ name: '从右缩小退出', value: 'zoomOutRight' },
{ name: '向上缩小退出', value: 'zoomOutUp' },
],
hiddenElement: []
},
{
type: 'slide',
name: '滑出',
children: [
{ name: '向下滑出', value: 'slideOutDown' },
{ name: '从左滑出', value: 'slideOutLeft' },
{ name: '从右滑出', value: 'slideOutRight' },
{ name: '向上滑出', value: 'slideOutUp' },
],
hiddenElement: []
},
{
type: 'flip',
name: '翻转',
children: [
{ name: 'X轴翻转退出', value: 'flipOutX' },
{ name: 'Y轴翻转退出', value: 'flipOutY' },
],
hiddenElement: []
},
{
type: 'back',
name: '缩小滑出',
children: [
{ name: '向下缩小滑出', value: 'backOutDown' },
{ name: '从左缩小滑出', value: 'backOutLeft' },
{ name: '从右缩小滑出', value: 'backOutRight' },
{ name: '向上缩小滑出', value: 'backOutUp' },
],
hiddenElement: []
},
{
type: 'lightSpeed',
name: '飞出',
children: [
{ name: '从右飞出', value: 'lightSpeedOutRight' },
{ name: '从左飞出', value: 'lightSpeedOutLeft' },
],
hiddenElement: []
}, },
] ]

View File

@ -58,8 +58,10 @@ export default () => {
element.id = elIdMap[element.id] element.id = elIdMap[element.id]
if (element.groupId) element.groupId = groupIdMap[element.groupId] if (element.groupId) element.groupId = groupIdMap[element.groupId]
} }
// 动画id替换
if (slide.animations) { if (slide.animations) {
for (const animation of slide.animations) { for (const animation of slide.animations) {
animation.id = nanoid(10)
animation.elId = elIdMap[animation.elId] animation.elId = elIdMap[animation.elId]
} }
} }

View File

@ -33,6 +33,7 @@ import {
Drawer, Drawer,
Spin, Spin,
Alert, Alert,
Tabs,
} from 'ant-design-vue' } from 'ant-design-vue'
const app = createApp(App) const app = createApp(App)
@ -62,6 +63,8 @@ app.component('Checkbox', Checkbox)
app.component('Drawer', Drawer) app.component('Drawer', Drawer)
app.component('Spin', Spin) app.component('Spin', Spin)
app.component('Alert', Alert) app.component('Alert', Alert)
app.component('Tabs', Tabs)
app.component('TabPane', Tabs.TabPane)
app.use(Icon) app.use(Icon)
app.use(Component) app.use(Component)

View File

@ -568,16 +568,28 @@ export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PP
/** /**
* *
* *
* id: 动画id
*
* elId: 元素ID * elId: 元素ID
* *
* type: * type:
* *
* effect: 动画效果
*
* duration: 动画持续时间 * duration: 动画持续时间
*
* delay: 动画延迟时间
*
* implement: 动画启动方式(0->1->2->)
*/ */
export interface PPTAnimation { export interface PPTAnimation {
id: string;
elId: string; elId: string;
type: string; type: string;
effect: string;
duration: number; duration: number;
delay: number;
implement: number;
} }
/** /**

View File

@ -4,6 +4,8 @@ export const enum EmitterEvents {
RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND', RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND',
OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR', OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR',
OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR', OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR',
RUN_ANIMATION = 'RUN_ANIMATION',
END_RUN_ANIMATION = 'END_RUN_ANIMATION',
} }
export interface RichTextAction { export interface RichTextAction {
@ -20,6 +22,8 @@ type Events = {
[EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand; [EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand;
[EmitterEvents.OPEN_CHART_DATA_EDITOR]: void; [EmitterEvents.OPEN_CHART_DATA_EDITOR]: void;
[EmitterEvents.OPEN_LATEX_EDITOR]: void; [EmitterEvents.OPEN_LATEX_EDITOR]: void;
[EmitterEvents.RUN_ANIMATION]: void;
[EmitterEvents.END_RUN_ANIMATION]: void;
} }
const emitter: Emitter<Events> = mitt<Events>() const emitter: Emitter<Events> = mitt<Events>()

View File

@ -4,44 +4,77 @@
<Popover <Popover
trigger="click" trigger="click"
v-model:visible="animationPoolVisible" v-model:visible="animationPoolVisible"
v-if="!['chart', 'video'].includes(handleElement.type)"
@visibleChange="visible => handlePopoverVisibleChange(visible)" @visibleChange="visible => handlePopoverVisibleChange(visible)"
> >
<template #content> <template #content>
<div class="animation-pool"> <Tabs v-model:activeKey="tabsActiveKey" tab-position="left" type="card">
<div class="pool-type" v-for="type in animations" :key="type.name"> <TabPane key="animation_in" tab="进场动效">
<div class="type-title">{{type.name}}</div> <div class="animation-pool">
<div class="pool-item-wrapper"> <div class="pool-type" :key="type.name" v-for="type in animations">
<div <div v-if="!type.hiddenElement.includes(handleElement.type)">
class="pool-item" <div class="type-title">{{type.name}}</div>
v-for="item in type.children" :key="item.name" <div class="pool-item-wrapper">
@mouseenter="hoverPreviewAnimation = item.value" <div
@mouseleave="hoverPreviewAnimation = ''" class="pool-item"
@click="addAnimation(item.value)" v-for="item in type.children" :key="item.name"
> @mouseenter="hoverPreviewAnimation = item.value"
<div @mouseleave="hoverPreviewAnimation = ''"
class="animation-box" @click="addAnimation(item.value, 'in')"
:class="[ >
'animate__animated', <div
'animate__faster', class="animation-box"
hoverPreviewAnimation === item.value && `animate__${item.value}`, :class="[
]" 'animate__animated',
> 'animate__faster',
{{item.name}} hoverPreviewAnimation === item.value && `animate__${item.value}`,
]"
>
{{item.name}}
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="mask" v-if="!popoverMaskHide"></div>
</div> </div>
</div> </TabPane>
<div class="mask" v-if="!popoverMaskHide"></div> <TabPane key="animation_out" tab="退场动效">
</div> <div class="animation-pool">
<div class="pool-type" :key="type.name" v-for="type in animationsExits">
<div v-if="!type.hiddenElement.includes(handleElement.type)">
<div class="type-title">{{type.name}}</div>
<div class="pool-item-wrapper">
<div
class="pool-item"
v-for="item in type.children" :key="item.name"
@mouseenter="onAnimationOut(item)"
@mouseleave="hoverPreviewAnimation = ''"
@click="addAnimation(item.value, 'out')"
>
<div
class="animation-box"
:class="[
'animate__animated',
'animate__faster',
hoverPreviewAnimation === item.value && `animate__${item.value}`,
]"
>
{{item.name}}
</div>
</div>
</div>
</div>
</div>
<div class="mask" v-if="!popoverMaskHide"></div>
</div>
</TabPane>
</Tabs>
</template> </template>
<Button class="element-animation-btn"> <Button class="element-animation-btn" @click="handleAnimationId = ''">
<IconEffects style="margin-right: 5px;" /> {{handleElementAnimationName || '点击选择动画'}} <IconEffects style="margin-right: 5px;" /> 添加动画
</Button> </Button>
</Popover> </Popover>
<Button class="element-animation-btn" v-else disabled>
<IconEffects style="margin-right: 5px;" /> 该元素暂不支持动画
</Button>
</div> </div>
<div class="tip" v-else><IconClick style="margin-right: 5px;" /> 选中画布中的元素添加动画</div> <div class="tip" v-else><IconClick style="margin-right: 5px;" /> 选中画布中的元素添加动画</div>
@ -59,50 +92,89 @@
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="sequence-item" :class="{ 'active': handleElement?.id === element.elId }"> <div class="sequence-item" :class="{ 'active': handleElement?.id === element.elId }">
<div class="index">{{index + 1}}</div> <div class="sequence-content">
<div class="text">{{element.elType}}{{element.animationType}}</div> <div class="index">{{index + 1}}</div>
<div class="handler"> <div class="text">{{element.elType}}{{element.animationType}}</div>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览"> <div class="handler">
<IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.type, element.duration)" /> <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览">
</Tooltip> <IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.type, element.duration, element.delay)" />
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除"> </Tooltip>
<IconCloseSmall class="handler-btn" @click="deleteAnimation(element.elId)" /> <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除">
</Tooltip> <IconCloseSmall class="handler-btn" @click="deleteAnimation(element.id)" />
</Tooltip>
</div>
</div>
<div class="configs" v-if="handleElementAnimation.length && handleElementAnimation[0].elId === element.elId">
<Divider />
<div class="duration">
<div style="flex: 3 1 0%;">动画时长</div>
<InputNumber
:min="100"
:max="5000"
:step="100"
:value="element.duration"
@change="value => updateElementAnimationDuration(element, value)"
style="flex: 4 1 20%;"
/>
<div class="duration-r"> 毫秒</div>
</div>
<div class="duration">
<div style="flex: 3;">动画启动方式</div>
<Select
v-model:value="element.implement"
@change="value => updateElementAnimationImplement(element, value)"
style="flex: 4;"
>
<SelectOption :value="0">单击时</SelectOption>
<SelectOption :value="1">与上一个一起</SelectOption>
<SelectOption :value="2">在上一个之后</SelectOption>
</Select>
</div>
<div class="duration" v-if="element.implement !== 1">
<div style="flex: 1;">延迟</div>
<InputNumber
:min="0"
:max="5000"
:step="100"
:value="element.delay"
@change="value => updateElementAnimationDelay(element, value)"
style="flex: 2;"
/>
<div class="duration-r">毫秒启动效果</div>
</div>
<div class="duration duration-btn">
<Button @click="handlePopoverVisibleChange(true), animationPoolVisible = true, handleAnimationId = element.id">更换特效</Button>
<Button @click="updateElementAnimationAll(element)">应用到本页所有</Button>
</div>
</div> </div>
</div> </div>
</template> </template>
</Draggable> </Draggable>
<div class="configs" v-if="handleElementAnimation">
<Divider />
<div class="duration">
<div style="flex: 4;">持续时间毫秒</div>
<InputNumber
:min="100"
:max="5000"
:step="100"
:value="handleElementAnimation.duration"
@change="value => updateElementAnimationDuration(value)"
style="flex: 3;"
/>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref } from 'vue' import { computed, defineComponent, ref, watch } from 'vue'
import { nanoid } from 'nanoid'
import emitter, { EmitterEvents } from '@/utils/emitter'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store' import { useMainStore, useSlidesStore } from '@/store'
import { PPTAnimation } from '@/types/slides' import { PPTAnimation } from '@/types/slides'
import { ANIMATIONS } from '@/configs/animation' import { ANIMATIONS, ANIMATIONS_EXITS } from '@/configs/animation'
import { ELEMENT_TYPE_ZH } from '@/configs/element' import { ELEMENT_TYPE_ZH } from '@/configs/element'
import useHistorySnapshot from '@/hooks/useHistorySnapshot' import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import { message } from 'ant-design-vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
const defaultDuration = 1000 const defaultDuration = 1000
const defaultDelay = 0
const defaultImplement = 0 // (0->1->2->)
const animationTypes: { [key: string]: string } = {} const animationTypes: { [key: string]: string } = {}
for (const type of ANIMATIONS) { for (const type of ANIMATIONS) {
@ -110,6 +182,11 @@ for (const type of ANIMATIONS) {
animationTypes[animation.value] = animation.name animationTypes[animation.value] = animation.name
} }
} }
for (const type of ANIMATIONS_EXITS) {
for (const animation of type.children) {
animationTypes[animation.value] = animation.name
}
}
export default defineComponent({ export default defineComponent({
name: 'element-animation-panel', name: 'element-animation-panel',
@ -121,12 +198,18 @@ export default defineComponent({
const { handleElement, handleElementId } = storeToRefs(useMainStore()) const { handleElement, handleElementId } = storeToRefs(useMainStore())
const { currentSlide, currentSlideAnimations } = storeToRefs(slidesStore) const { currentSlide, currentSlideAnimations } = storeToRefs(slidesStore)
watch(() => handleElementId.value, () => {
animationPoolVisible.value = false
})
const hoverPreviewAnimation = ref('') const hoverPreviewAnimation = ref('')
const tabsActiveKey = ref('animation_in')
const animationPoolVisible = ref(false) const animationPoolVisible = ref(false)
const { addHistorySnapshot } = useHistorySnapshot() const { addHistorySnapshot } = useHistorySnapshot()
const animations = ANIMATIONS const animations = ANIMATIONS
const animationsExits = ANIMATIONS_EXITS
// //
const animationSequence = computed(() => { const animationSequence = computed(() => {
@ -137,7 +220,6 @@ export default defineComponent({
if (!el) continue if (!el) continue
const elType = ELEMENT_TYPE_ZH[el.type] const elType = ELEMENT_TYPE_ZH[el.type]
const animationType = animationTypes[animation.type] const animationType = animationTypes[animation.type]
animationSequence.push({ animationSequence.push({
...animation, ...animation,
elType, elType,
@ -150,19 +232,13 @@ export default defineComponent({
// //
const handleElementAnimation = computed(() => { const handleElementAnimation = computed(() => {
const animations = currentSlideAnimations.value || [] const animations = currentSlideAnimations.value || []
const animation = animations.find(item => item.elId === handleElementId.value) const animation = animations.filter(item => item.elId === handleElementId.value)
return animation || null return animation || []
})
//
const handleElementAnimationName = computed(() => {
if (!handleElementAnimation.value) return null
return animationTypes[handleElementAnimation.value.type]
}) })
// //
const deleteAnimation = (elId: string) => { const deleteAnimation = (id: string) => {
const animations = (currentSlideAnimations.value as PPTAnimation[]).filter(item => item.elId !== elId) const animations = (currentSlideAnimations.value as PPTAnimation[]).filter(item => item.id !== id)
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
addHistorySnapshot() addHistorySnapshot()
} }
@ -182,28 +258,89 @@ export default defineComponent({
} }
// //
const runAnimation = (elId: string, animationType: string, duration: number) => { const runAnimation = (elId: string, animationType: string, duration: number, delay: number) => {
const prefix = 'animate__' const prefix = 'animate__'
const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`) const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`)
if (elRef) { if (elRef) {
const animationName = `${prefix}${animationType}` setTimeout(() => {
document.documentElement.style.setProperty('--animate-duration', `${duration}ms`) const animationName = `${prefix}${animationType}`
elRef.classList.add(`${prefix}animated`, animationName) document.documentElement.style.setProperty('--animate-duration', `${duration}ms`)
elRef.classList.add(`${prefix}animated`, animationName)
const handleAnimationEnd = () => { emitter.emit(EmitterEvents.RUN_ANIMATION)
document.documentElement.style.removeProperty('--animate-duration')
elRef.classList.remove(`${prefix}animated`, animationName) const handleAnimationEnd = () => {
} document.documentElement.style.removeProperty('--animate-duration')
elRef.addEventListener('animationend', handleAnimationEnd, { once: true }) elRef.classList.remove(`${prefix}animated`, animationName)
emitter.emit(EmitterEvents.END_RUN_ANIMATION)
}
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
}, delay)
} }
} }
// //
const updateElementAnimation = (type: string) => { const updateElementAnimationDuration = (element: { id: string }, duration: number) => {
if (!currentSlideAnimations.value) return
if (duration < 100 || duration > 5000) return
const animations = currentSlideAnimations.value.map(item => {
if (item.id === element.id) return { ...item, duration }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimationImplement = (element: { id: string }, implement: number) => {
if (!currentSlideAnimations.value) return if (!currentSlideAnimations.value) return
const animations = currentSlideAnimations.value.map(item => { const animations = currentSlideAnimations.value.map(item => {
if (item.elId === handleElementId.value) return { ...item, type } if (item.id === element.id) return { ...item, implement }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimationDelay = (element: { id: string }, delay: number) => {
if (!currentSlideAnimations.value) return
if (delay < 0 || delay > 5000) return
const animations = currentSlideAnimations.value.map(item => {
if (item.id === element.id) return { ...item, delay }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimationAll = (element: { duration: number; delay: number; implement: number }) => {
if (!handleElementAnimation.value) return
const animations: PPTAnimation[] = currentSlideAnimations.value ? JSON.parse(JSON.stringify(currentSlideAnimations.value)) : []
const handleElementAnimationMap = {
duration: element.duration,
delay: element.delay,
implement: element.implement
}
const animationsUpdate: PPTAnimation[] = animations.map(x => Object.assign({}, x, handleElementAnimationMap))
slidesStore.updateSlide({ animations: animationsUpdate })
addHistorySnapshot()
message.success({ content: '已应用本页其他动画', duration: 3 })
}
//
const updateElementAnimation = (type: string, effect: string) => {
if (!currentSlideAnimations.value) return
const animations = currentSlideAnimations.value.map(item => {
if (item.id === handleAnimationId.value) return { ...item, type, effect }
return item return item
}) })
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
@ -212,40 +349,34 @@ export default defineComponent({
const animationItem = currentSlideAnimations.value.find(item => item.elId === handleElementId.value) const animationItem = currentSlideAnimations.value.find(item => item.elId === handleElementId.value)
const duration = animationItem?.duration || defaultDuration const duration = animationItem?.duration || defaultDuration
const delay = animationItem?.delay || defaultDelay
runAnimation(handleElementId.value, type, duration) runAnimation(handleElementId.value, type, duration, delay)
}
//
const updateElementAnimationDuration = (duration: number) => {
if (!currentSlideAnimations.value) return
if (duration < 100 || duration > 5000) return
const animations = currentSlideAnimations.value.map(item => {
if (item.elId === handleElementId.value) return { ...item, duration }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
} }
const handleAnimationId = ref('')
// //
const addAnimation = (type: string) => { const addAnimation = (type: string, effect: string) => {
if (handleElementAnimationName.value) { if (handleAnimationId.value) {
updateElementAnimation(type) updateElementAnimation(type, effect)
return return
} }
const animations: PPTAnimation[] = currentSlideAnimations.value ? JSON.parse(JSON.stringify(currentSlideAnimations.value)) : [] const animations: PPTAnimation[] = currentSlideAnimations.value ? JSON.parse(JSON.stringify(currentSlideAnimations.value)) : []
animations.push({ animations.push({
id: nanoid(10),
elId: handleElementId.value, elId: handleElementId.value,
type, type,
effect,
duration: defaultDuration, duration: defaultDuration,
delay: defaultDelay,
implement: defaultImplement,
}) })
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
animationPoolVisible.value = false animationPoolVisible.value = false
addHistorySnapshot() addHistorySnapshot()
runAnimation(handleElementId.value, type, defaultDuration) runAnimation(handleElementId.value, type, defaultDuration, defaultDelay)
} }
// 500ms // 500ms
@ -257,21 +388,41 @@ export default defineComponent({
else popoverMaskHide.value = false else popoverMaskHide.value = false
} }
const animationOutItem = ref({})
const animationOutTimer = ref(0)
const onAnimationOut = (item: { value: string }) => {
if (item.value === animationOutItem.value) {
return
}
clearTimeout(animationOutTimer.value)
animationOutItem.value = item
hoverPreviewAnimation.value = item.value
animationOutTimer.value = window.setTimeout(() => {
hoverPreviewAnimation.value = ''
}, 500)
}
return { return {
handleAnimationId,
handleElement, handleElement,
animationPoolVisible, animationPoolVisible,
animations, animations,
animationsExits,
animationSequence, animationSequence,
hoverPreviewAnimation, hoverPreviewAnimation,
handleElementAnimation, handleElementAnimation,
handleElementAnimationName,
popoverMaskHide, popoverMaskHide,
addAnimation, addAnimation,
deleteAnimation, deleteAnimation,
handleDragEnd, handleDragEnd,
runAnimation, runAnimation,
updateElementAnimationDuration, updateElementAnimationDuration,
updateElementAnimationImplement,
updateElementAnimationDelay,
handlePopoverVisibleChange, handlePopoverVisibleChange,
updateElementAnimationAll,
tabsActiveKey,
onAnimationOut,
} }
}, },
}) })
@ -292,9 +443,19 @@ export default defineComponent({
width: 100%; width: 100%;
} }
.duration { .duration {
margin-top: 5px;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
word-break: keep-all;
.duration-r {
flex: 2;
text-indent: 8px;
}
&.duration-btn {
margin-top: 20px;
justify-content: space-between;
}
} }
.tip { .tip {
height: 32px; height: 32px;
@ -310,7 +471,6 @@ export default defineComponent({
overflow-x: hidden; overflow-x: hidden;
font-size: 12px; font-size: 12px;
margin-right: -12px; margin-right: -12px;
padding-right: 12px;
position: relative; position: relative;
.mask { .mask {
@ -353,13 +513,12 @@ export default defineComponent({
@include overflow-overlay(); @include overflow-overlay();
} }
.sequence-item { .sequence-item {
height: 36px; // height: 35px;
display: flex;
align-items: center;
border: 1px solid $borderColor; border: 1px solid $borderColor;
padding: 6px; padding: 9px 6px;
border-radius: $borderRadius; border-radius: $borderRadius;
margin-bottom: 8px; margin-bottom: 8px;
transition: all .5s;
cursor: grab; cursor: grab;
&:active { &:active {
@ -368,22 +527,28 @@ export default defineComponent({
&.active { &.active {
border-color: $themeColor; border-color: $themeColor;
height: auto;
} }
.index { .sequence-content {
flex: 1; display: flex;
} align-items: center;
.text { // height: 23px;
flex: 6; .index {
} flex: 1;
.handler { }
flex: 2; .text {
font-size: 15px; flex: 6;
text-align: right; }
} .handler {
.handler-btn { flex: 2;
margin-left: 8px; font-size: 15px;
cursor: pointer; text-align: right;
}
.handler-btn {
margin-left: 8px;
cursor: pointer;
}
} }
} }
</style> </style>

View File

@ -81,8 +81,41 @@ export default defineComponent({
const needWaitAnimation = computed(() => { const needWaitAnimation = computed(() => {
const animations = currentSlide.value.animations || [] const animations = currentSlide.value.animations || []
const elementIndexInAnimation = animations.findIndex(animation => animation.elId === props.elementInfo.id) const elementIndexInAnimation = animations.findIndex(animation => animation.elId === props.elementInfo.id)
if (elementIndexInAnimation !== -1 && elementIndexInAnimation >= props.animationIndex) return true if (elementIndexInAnimation === -1) {
return false return false
}
// effect in
const firstAnimation = animations.find(animation => animation.elId === props.elementInfo.id)
if (!firstAnimation) {
return false
}
// animationIndex
const findAnimationsList = animations.filter((animation, index) => index < props.animationIndex && animation.elId === props.elementInfo.id)
if (findAnimationsList.length) {
const lastAnimation = findAnimationsList[findAnimationsList.length - 1]
const effect = lastAnimation.effect || 'in' // VERSION 2022/5/7 : effect
if (effect === 'out') {
return false
}
if (elementIndexInAnimation >= props.animationIndex) {
return true
}
return false
}
// effect
const firstEffectAnimation = animations.find(animation => animation.elId === props.elementInfo.id)
if (firstEffectAnimation) {
const effect = firstEffectAnimation.effect || 'in' // VERSION 2022/5/7 : effect
if (effect === 'out') {
return false
}
return true
}
return true
}) })
// //

View File

@ -14,31 +14,80 @@ export default () => {
const animations = computed(() => currentSlide.value.animations || []) const animations = computed(() => currentSlide.value.animations || [])
const animationIndex = ref(0) const animationIndex = ref(0)
const awaitRef = ref<number | null>(-1)
const awaitTimer = (delay: number) => {
return new Promise((resolve) => {
awaitRef.value = window.setTimeout(() => {
awaitRef.value = null
resolve(delay)
}, delay)
})
}
// 执行元素的入场动画 // 执行元素的入场动画
const runAnimation = () => { const runAnimation = async () => {
const prefix = 'animate__' const prefix = 'animate__'
const animation = animations.value[animationIndex.value] const animation = animations.value[animationIndex.value]
animationIndex.value += 1
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`) const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (elRef) { if (elRef) {
if (awaitRef.value) {
clearTimeout(awaitRef.value)
awaitRef.value = null
}
else if (animation.implement !== 1) {
// 判断执行动画 与上一动画在一起则不延迟
await awaitTimer(animation.delay || 0)
}
// eslint-disable-next-line require-atomic-updates
animationIndex.value += 1
const animationName = `${prefix}${animation.type}` const animationName = `${prefix}${animation.type}`
document.documentElement.style.setProperty('--animate-duration', `${animation.duration}ms`) document.documentElement.style.setProperty('--animate-duration', `${animation.duration}ms`)
elRef.classList.add(`${prefix}animated`, animationName)
// 判断如果存在非进场动画保留,就去除原动画
elRef.classList.remove(`${prefix}animated`)
for (let i = 0; i < elRef.classList.length; i++) {
if (elRef.classList[i].indexOf(prefix) > -1) {
elRef.classList.remove(elRef.classList[i])
}
}
elRef.classList.add(animationName, `${prefix}animated`)
const handleAnimationEnd = () => { const handleAnimationEnd = () => {
document.documentElement.style.removeProperty('--animate-duration') document.documentElement.style.removeProperty('--animate-duration')
elRef.classList.remove(`${prefix}animated`, animationName) if (animation.effect === 'in') { // 如果是进场动画就去除动画
elRef.classList.remove(`${prefix}animated`)
elRef.classList.remove(animationName)
}
} }
elRef.addEventListener('animationend', handleAnimationEnd, { once: true }) elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
// 判断下个动画
if (animations.value.length && animationIndex.value < animations.value.length) {
const nextAnimation = animations.value[animationIndex.value]
if (nextAnimation.implement === 1) { // 一起
runAnimation()
}
else if (nextAnimation.implement === 2) { // 之后
await awaitTimer(animation.duration || 0)
runAnimation()
}
}
}
else {
animationIndex.value += 1
runAnimation()
} }
} }
// 关闭自动播放 // 关闭自动播放
const autoPlayTimer = ref(0) const autoPlayTimer = ref<number>(0)
const closeAutoPlay = () => { const closeAutoPlay = () => {
if (autoPlayTimer.value) { if (autoPlayTimer.value) {
clearInterval(autoPlayTimer.value) window.clearInterval(autoPlayTimer.value)
autoPlayTimer.value = 0 autoPlayTimer.value = 0
} }
} }
@ -54,6 +103,21 @@ export default () => {
const execPrev = () => { const execPrev = () => {
if (animations.value.length && animationIndex.value > 0) { if (animations.value.length && animationIndex.value > 0) {
animationIndex.value -= 1 animationIndex.value -= 1
// 判断当前动画是否保留动画效果,有则去除后再上翻
const prefix = 'animate__'
const animation = animations.value[animationIndex.value]
if (animation) {
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (elRef) {
elRef.classList.remove(`${prefix}animated`)
for (let i = 0; i < elRef.classList.length; i++) {
if (elRef.classList[i].indexOf(prefix) > -1) {
elRef.classList.remove(elRef.classList[i])
}
}
}
}
} }
else if (slideIndex.value > 0) { else if (slideIndex.value > 0) {
slidesStore.updateSlideIndex(slideIndex.value - 1) slidesStore.updateSlideIndex(slideIndex.value - 1)
@ -82,7 +146,7 @@ export default () => {
const autoPlay = () => { const autoPlay = () => {
closeAutoPlay() closeAutoPlay()
message.success('开始自动放映') message.success('开始自动放映')
autoPlayTimer.value = setInterval(execNext, 2500) autoPlayTimer.value = window.setInterval(execNext, 2500)
} }
// 鼠标滚动翻页 // 鼠标滚动翻页