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>

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

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁2 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄2 天前
JavaScript 隐式全局变量解析
javascript
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大2 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师2 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端