CSS+JS实现单例老虎机切换图片动画

最近实现了一个图片的切换展示,中间想加一个过渡动画,于是就想到老虎机的效果:

快速开始滚动后逐渐减速定格,定格前上下晃动,下面四张图给大家用:

先看效果:

我的代码将会做什么?

  • 图片顺序固定(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?
javascript 复制代码
const renderList = computed(() =>
  Array.from({ length: domLoops }).flatMap(() => baseList)
);

制造"无限滚动"的视觉假象,虽然只有4张图,但给人一种有无限张图片的感觉,不会滚到尽头导致空白。

  • 为什么需要 safeBase?
javascript 复制代码
const totalHeight = starMax * imgHeight;
const safeBase = totalHeight * 2;

把 reel 的初始位置放在 DOM 中间的"安全区"

javascript 复制代码
[重复区][安全区][重复区]
          ↑
      初始位置

这样可以实现 向上滚、向下滚都不会立刻碰到 DOM 边界

  • 目标 Y 的统一计算函数
javascript 复制代码
function calcTargetY(index, extraLoops = 0) {
  return -(safeBase + (extraLoops * starMax + index) * imgHeight);
}

最终一定精准对齐到某一张图,我这里是想顺序的去访问着四张图,而不是随机展示

javascript 复制代码
const toIndex = (currentIndex.value + 1) % starMax;

这里就是写的顺序,如果想随机,就可以写成:

javascript 复制代码
const toIndex = Math.floor(Math.random() * starMax);
  • 初始化
javascript 复制代码
const 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>

以上就是全部效果的代码展示~

相关推荐
坐公交也用券7 小时前
适用于vue3+pnpm项目自动化类型检查及构建的Python脚本
开发语言·javascript·python·typescript·自动化
IT_陈寒7 小时前
Vue3性能优化实战:这5个技巧让我的应用加载速度提升了40%
前端·人工智能·后端
小小鸟0087 小时前
Vue响应式原理
前端·javascript·vue.js
lee5768 小时前
鄙人的 Vue 3.0 商业级开源甘特图已经发布到 npm
前端·vue.js·npm·开源·甘特图
方圆工作室8 小时前
纯HTML/CSS健康数据分析平台
css·数据分析·html
前端老曹8 小时前
vue3 三级路由无法缓存的终终终终终终极解决方案
前端·javascript·vue.js
零Suger8 小时前
React Router v7数据模式使用指南
javascript·笔记·react.js
1024小神8 小时前
uniapp + vue3 + scss 定义全局样式变量,并使用
前端·uni-app·scss
顾安r8 小时前
12.17 脚本网页 创意导航
java·linux·前端·游戏·html