最近实现了一个图片的切换展示,中间想加一个过渡动画,于是就想到老虎机的效果:
快速开始滚动后逐渐减速定格,定格前上下晃动,下面四张图给大家用:




先看效果:

我的代码将会做什么?
- 图片顺序固定(1 → 2 → 3 → 4 → 循环)
- 初始化时 第一张也有滚动动画
- 每次点击向下滚动若干圈后定格到下一张
- 定格时带有物理感的上下晃动
- 不出现空白、不闪烁、不跳图
下面一起来看:
html
<template>
<div class="container">
<div class="viewport">
<div class="reel" :style="{ transform: `translateY(${translateY}px)` }">
<img v-for="(src, idx) in renderList" :key="idx" class="star-img" :src="src" draggable="false" alt="" />
</div>
</div>
<img class="bottom-btn-inside" :src="btn" @click="startRoll" alt=""/>
</div>
</template>
javascript
<script setup>
import { ref, computed, onMounted } from "vue";
import img1 from './temp/1.png'
import img2 from './temp/2.png'
import img3 from './temp/3.png'
import img4 from './temp/4.png'
import btn from './temp/btn.png'
const baseList = [img1, img2, img3, img4]
const starMax = 4;
const imgHeight = 300;
const domLoops = 6;
const rollLoops = 3;
const translateY = ref(0);
const currentIndex = ref(-1);
const rolling = ref(false);
const renderList = computed(() =>
Array.from({ length: domLoops }).flatMap(() => baseList)
);
const totalHeight = starMax * imgHeight;
const safeBase = totalHeight * 2;
function calcTargetY(index, extraLoops = 0) {
return -(safeBase + (extraLoops * starMax + index) * imgHeight);
}
onMounted(() => {
translateY.value = calcTargetY(0);
requestAnimationFrame(() => startRoll(true));
});
function startRoll(isInit = false) {
if (rolling.value) return;
rolling.value = true;
const toIndex = (currentIndex.value + 1) % starMax;
const startY = translateY.value;
const targetY = calcTargetY(toIndex, rollLoops);
const duration = isInit ? 2200 : 2600;
const startTime = performance.now();
function animate(now) {
const t = Math.min((now - startTime) / duration, 1);
const eased = easeOutQuint(t);
translateY.value = startY + (targetY - startY) * eased;
if (t < 1) {
requestAnimationFrame(animate);
} else {
currentIndex.value = toIndex;
smoothShake(calcTargetY(toIndex), () => {
translateY.value = calcTargetY(toIndex);
rolling.value = false;
});
}
}
requestAnimationFrame(animate);
}
function smoothShake(baseY, done) {
const amplitude = 12;
const damping = 5;
const frequency = 25;
const duration = 600;
const start = performance.now();
function animate(now) {
const t = (now - start) / 1000;
if (t > duration / 1000) {
translateY.value = baseY;
done();
return;
}
const offset =
amplitude *
Math.exp(-damping * t) *
Math.sin(frequency * t);
translateY.value = baseY + offset;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
function easeOutQuint(t) {
return 1 - Math.pow(1 - t, 5);
}
</script>
- 为什么要用 renderList = baseList × domLoops?
javascriptconst renderList = computed(() => Array.from({ length: domLoops }).flatMap(() => baseList) );制造"无限滚动"的视觉假象,虽然只有4张图,但给人一种有无限张图片的感觉,不会滚到尽头导致空白。
- 为什么需要 safeBase?
javascriptconst totalHeight = starMax * imgHeight; const safeBase = totalHeight * 2;把 reel 的初始位置放在 DOM 中间的"安全区":
javascript[重复区][安全区][重复区] ↑ 初始位置这样可以实现 向上滚、向下滚都不会立刻碰到 DOM 边界
- 目标 Y 的统一计算函数
javascriptfunction calcTargetY(index, extraLoops = 0) { return -(safeBase + (extraLoops * starMax + index) * imgHeight); }最终一定精准对齐到某一张图,我这里是想顺序的去访问着四张图,而不是随机展示
javascriptconst toIndex = (currentIndex.value + 1) % starMax;这里就是写的顺序,如果想随机,就可以写成:
javascriptconst toIndex = Math.floor(Math.random() * starMax);
- 初始化
javascriptconst currentIndex = ref(-1); const toIndex = (currentIndex.value + 1) % starMax; // -1 + 1 = 0 onMounted(() => { translateY.value = calcTargetY(0); requestAnimationFrame(() => startRoll(true)); });初始化设置为 -1,就可以一开始滚动到第一张
- 其他就是设置多轮滚动、滚动效果、定格效果...
css
<style scoped>
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.viewport {
width: 300px;
height: 300px;
overflow: hidden;
position: relative;
border: 1px solid grey
}
.reel {
position: absolute;
top: 0;
left: 0;
will-change: transform;
}
.star-img {
width: 300px;
height: 300px;
display: block;
}
.bottom-btn-inside {
margin-top: 20px;
width: 140px;
height: 80px;
cursor: pointer;
}
</style>
以上就是全部效果的代码展示~