Skip to content

Commit 3453cda

Browse files
committed
feat: 序列动画的进度条(现在有点小bug,对不上点
1 parent b01ab26 commit 3453cda

5 files changed

Lines changed: 90 additions & 14 deletions

File tree

app/components/animate/AnimateHandler.vue

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,30 @@
22
<div ref="container" :style="{ opacity: 0 }" class="w-full h-full flex items-center justify-center -z-100">
33
<slot />
44
</div>
5+
<div v-if="showProgressBar" class="fixed bottom-0 left-0 w-full h-2">
6+
<motion.div
7+
class="w-full h-2 bg-blue-200 transition-all"
8+
:style="{ width: progressPercent }"
9+
/>
10+
</div>
511
</template>
612

713
<script setup lang="ts">
814
import type { AnimationSequence } from 'motion-v';
9-
import { animate } from 'motion-v';
15+
import { animate, motion } from 'motion-v';
1016
1117
const props = defineProps<{
1218
initSequence: () => AnimationSequence; // 初始化序列
1319
enterSequence: () => AnimationSequence; // 进入动画序列
1420
exitSequence: () => AnimationSequence; // 退出动画序列
1521
infinitySequence?: () => AnimationSequence; // 无限循环动画序列
1622
17-
enterDelay?: number; // 进入动画延迟 毫秒
18-
exitDelay?: number; // 退出动画延迟 毫秒
23+
enterDelay?: number; // 进入动画延迟 秒
24+
exitDelay?: number; // 退出动画延迟 秒
25+
26+
showProgressBar?: boolean; // 是否显示进度条
27+
enterDuration?: number; // 进入动画持续时间(秒)
28+
exitDuration?: number; // 退出动画持续时间(秒)
1929
}>();
2030
2131
const emit = defineEmits<{
@@ -36,6 +46,11 @@ const aniStatus = reactive<AnimateStatus>({
3646
stage: 'init',
3747
status: 'done',
3848
});
49+
const progressValue = useMotionValue(0);
50+
// useMotionValueEvent(progressValue, 'change', (latest) => {
51+
// console.log(`Progress: ${latest}`);
52+
// });
53+
const progressPercent = useTransform(progressValue, value => `${value * 100}%`);
3954
4055
async function runSequence(seq: AnimationSequence, options?: { duration?: number }) {
4156
if (!seq)
@@ -69,9 +84,12 @@ async function runEnterSequence() {
6984
const seq: AnimationSequence = [
7085
[container.value!, { opacity: [0, 1], y: ['100vh', 0] }, { at: '0', duration: 0.5 }],
7186
];
87+
7288
await runSequence(seq);
73-
if (props.enterSequence)
89+
if (props.enterSequence) {
90+
animate(progressValue, 0.5, { duration: getSequenceDuration(props.enterSequence()), ease: 'linear' });
7491
await runSequence(props.enterSequence(), {});
92+
}
7593
emit('enterComplete');
7694
aniStatus.stage = 'exit';
7795
aniStatus.status = 'done';
@@ -84,12 +102,15 @@ async function runExitSequence() {
84102
aniStatus.stage = 'exit';
85103
aniStatus.status = 'running';
86104
emit('exitStart');
105+
87106
if (props.exitSequence) {
107+
animate(progressValue, 1, { duration: getSequenceDuration(props.exitSequence()), ease: 'linear' });
88108
await runSequence(props.exitSequence());
89109
}
90110
const seq: AnimationSequence = [
91111
[container.value!, { opacity: [1, 0], y: [0, '-100vh'] }, { at: '+0', duration: 1 }],
92112
];
113+
93114
await runSequence(seq, {});
94115
emit('exitComplete');
95116
aniStatus.stage = 'exit';
@@ -99,15 +120,62 @@ async function runExitSequence() {
99120
async function runFullSequence() {
100121
await runInitSequence();
101122
// console.log('Init Sequence Completed');
102-
if (props.enterDelay)
103-
await new Promise(resolve => setTimeout(resolve, props.enterDelay));
123+
if (typeof props.enterDelay)
124+
await new Promise(resolve => setTimeout(resolve, (props.enterDelay ?? 0) * 1000));
104125
await runEnterSequence();
105126
// console.log('Enter Sequence Completed');
106-
if (props.exitDelay)
107-
await new Promise(resolve => setTimeout(resolve, props.exitDelay));
127+
if (typeof props.exitDelay)
128+
await new Promise(resolve => setTimeout(resolve, (props.exitDelay ?? 0) * 1000));
108129
await runExitSequence();
109130
}
110131
132+
function getSequenceDuration(sequence: AnimationSequence): number {
133+
let prevStart = 0;
134+
let prevEnd = 0;
135+
let totalDuration = 0;
136+
for (const item of sequence) {
137+
const [, , options = {}] = item as any;
138+
const { at, duration = 0.3, delay = 0 } = options as any;
139+
140+
// 处理 delay 可能是函数(例如 stagger)
141+
let delayNum = 0;
142+
if (typeof delay === 'function') {
143+
delayNum = 0.1;
144+
} else {
145+
delayNum = delay || 0;
146+
}
147+
148+
// 解析 at
149+
let start: number;
150+
if (at === undefined) {
151+
start = prevEnd;
152+
} else if (typeof at === 'number') {
153+
start = at;
154+
} else if (typeof at === 'string') {
155+
if (at === '<') {
156+
start = prevStart;
157+
} else if (at.startsWith('+')) {
158+
start = prevEnd + Number.parseFloat(at);
159+
} else if (at.startsWith('-')) {
160+
start = prevEnd - Number.parseFloat(at);
161+
} else {
162+
// 标签等忽略
163+
start = prevEnd;
164+
}
165+
} else {
166+
start = prevEnd;
167+
}
168+
169+
start += delayNum;
170+
const end = start + duration;
171+
totalDuration = Math.max(totalDuration, end);
172+
prevStart = start;
173+
prevEnd = end;
174+
}
175+
// console.log(`Calculated sequence duration: ${totalDuration}s`);
176+
return totalDuration;
177+
}
178+
111179
defineExpose({
112180
runFullSequence,
113181
runInitSequence,

app/components/animate/FirstReason.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
['.minor-intro-text', { opacity: [1, 0], y: [0, 50] }, { duration: 0.2, at: '<' }],
2424
[{ opacity: [1, 0] }, { duration: 0.3, at: '+1' }],
2525
]"
26+
:show-progress-bar="true"
27+
:enter-duration="2"
28+
:exit-duration="1"
2629

27-
:enter-delay="5000"
28-
:exit-delay="3000"
30+
:enter-delay="3"
31+
:exit-delay="3"
2932

3033
@init-complete="console.log('Init Complete')"
3134
@enter-start="() => {

app/components/animate/SecondReason.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
[imgBox1, { opacity: [1, 0], y: [0, -200], scale: [1, 0.4] }, { duration: 0.3, at: '+0.1' }],
2828
['.minor-intro-text', { opacity: [1, 0], y: [0, 50] }, { duration: 0.3, at: '+0.2' }],
2929
]"
30+
:enter-duration="2"
31+
:exit-duration="1"
3032
@init-complete="console.log('Init Complete')"
3133
@enter-start="() => console.log('Enter Start')"
3234
@enter-complete="console.log('Enter Complete')"

app/components/animate/ThirdReason.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
['.minor-intro-text', { opacity: [1, 0], y: [0, 50] }, { duration: 0.3, at: '+0.1' }],
2222
[imgBox1, { opacity: [1, 0], y: [0, -200], scale: [1, 0.4] }, { duration: 0.4, at: '+0.2' }],
2323
]"
24-
:exit-delay="10000"
24+
:exit-delay="5"
25+
:show-progress-bar="true"
26+
:enter-duration="4"
27+
:exit-duration="5"
2528
@init-complete="console.log('Init Complete')"
2629
@enter-start="() => {
2730
console.log('Enter Start')

app/pages/animate.test.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<ThirdReason
2+
<FirstReason
33
v-if="stage === 0"
44
ref="sReasonRef"
55
title1="为什么选择我们?"
@@ -10,14 +10,14 @@
1010
</template>
1111

1212
<script lang="ts" setup>
13-
import ThirdReason from '@/components/animate/ThirdReason.vue';
13+
import FirstReason from '@/components/animate/FirstReason.vue';
1414
1515
definePageMeta({
1616
layout: 'fullscreen',
1717
});
1818
1919
const stage = ref(0);
20-
const sReasonRef = ref<InstanceType<typeof ThirdReason>>();
20+
const sReasonRef = ref<InstanceType<typeof FirstReason>>();
2121
2222
onMounted(() => {
2323
sReasonRef.value?.playFull();

0 commit comments

Comments
 (0)