妹有模型,只有序列帧,怎么实现“伪3D”?

218 阅读7分钟

引言

你在学three时是否苦于没有3d模型?但是业务又需要实现一个模型的旋转,组长啪的一扔,这里面是序列帧,立马给我实现伪3D的旋转起来!干不了不给你下播!可恶,没有模型,看着这50张的序列帧,想到我们的电影也是一帧一帧的图片组成的动画,连起来就相当于也是动起来啦!所以饿着肚子,吃掉了这50张图片。反手网上搜索例子,也是吃饱了。


一、核心概念:什么是"交互式序列帧动画"?

1.1 序列帧动画:会"变戏法"的图片集

序列帧动画(Sprite Animation)是一种经典的动画技术,原理简单粗暴:​​用多张连续的静态图片,通过快速切换形成动态效果​​。比如你小时候看的《黑猫警长》,里面的角色其实就是几张不同动作的小图片按顺序播放。

在我们的案例里,小棕熊有49张"GIF表情包"(frame-1.gifframe-49.gif):第1张是"抱蜂蜜发呆",第15张是"歪头盯蜂蜜",第30张是"爪子扒拉罐子",每张图片记录一个微小的动作变化。

1.2 交互式控制:让用户当"训熊师"

普通的序列帧动画是"自动播放"的(比如循环扒拉蜂蜜罐),但我们的目标是让用户​​用鼠标/手指"指挥"小棕熊动作​​:拖动向左,小棕熊逆时针歪头;拖动向右,小棕熊顺时针晃爪子。这就像给小棕熊装了一个"遥控器",用户说怎么动,小棕熊就怎么动。


二、代码拆解:Vue 3如何"驯服"小棕熊?

2.1 准备工作:给小棕熊建个"动作库"

首先,我们需要让Vue知道小棕熊有多少张"动作包",以及如何按顺序找到它们。代码里的totalFrames = 49就是告诉Vue:"嘿,小棕熊有49张动作包,编号1到49!"(注意:图片文件从frame-1.gif开始,所以索引要+1)

// 序列帧相关
const totalFrames = 49; // 1-49共49帧(图片文件:frame-1.gif到frame-49.gif)
const currentFrameIndex = ref(0); // 当前帧索引(初始是0→对应第1张"抱蜂蜜发呆")
const currentFrame = ref(''); // 当前显示的图片路径

2.2 核心逻辑:拖动→计算→换图

整个交互的灵魂在onDrag函数——它负责把用户的拖动动作翻译成"小棕熊该翻到第几张动作包"。我们一步步拆解:

(1)监听拖动事件:鼠标和触摸屏通吃

Vue的模板里绑定了@mousedown@mousemove等事件,相当于给小棕熊容器装了"顺风耳"——无论是用鼠标按住拖动,还是用手指在手机上滑,它都能"听"到。

<div 
  class="teapot-container"  <!-- 注意类名还是"teapot",但实际是小棕熊~ -->
  @mousedown="startDrag"    <!-- 鼠标按下:准备拖动 -->
  @mousemove="onDrag"       <!-- 鼠标移动:正在拖动 -->
  @mouseup="endDrag"        <!-- 鼠标松开:停止拖动 -->
  @touchstart="startDrag"   <!-- 触摸开始:手机拖动准备 -->
  @touchmove="onDrag"       <!-- 触摸移动:手机正在拖动 -->
  @touchend="endDrag">      <!-- 触摸结束:手机停止拖动 -->
  <img :src="currentFrame" alt="小棕熊" class="teapot-model" />  <!-- 图片标签 -->
</div>

(2)startDrag:按下时"锁定目标"

当用户按下鼠标或触摸屏幕时,startDrag会被触发。它的任务是:

  • 记录当前拖动的起点坐标(startXlastX)。
  • 关闭之前的动画帧更新(避免"手忙脚乱")。
const startDrag = event => {
  isDragging.value = true; // 标记"正在拖动"
  // 根据事件类型获取客户端X坐标(兼容鼠标和触摸)
  const clientX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX;
  startX.value = clientX; // 记录按下时的X坐标
  lastX.value = clientX;  // 记录上一次移动的X坐标(初始和起点相同)
  // 取消之前可能残留的动画帧更新(避免重复指令)
  if (animationFrameId.value) {
    animationFrameId.value = null;
    pendingUpdate.value = false;
  }
  event.preventDefault(); // 阻止默认行为(比如页面滚动)
};

(3)onDrag:移动时"计算步数"

用户拖动时,onDrag会不断被触发。它的核心逻辑是:

  • 计算当前拖动位置与上一次位置的差值(deltaX)。
  • 根据差值和灵敏度(sensitivity=5),决定小棕熊该"换"多少张动作包。
const onDrag = event => {
  if (!isDragging.value) return; // 没在拖动?直接溜了~
  // 获取当前鼠标/触摸的X坐标
  const clientX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX;
  const deltaX = clientX - lastX.value; // 当前位置 - 上一次位置 = 拖动距离

  // 只有拖动距离超过灵敏度(5px)时,才触发换图(避免"手抖"误触)
  if (Math.abs(deltaX) >= sensitivity) {
    const frameChange = Math.floor(deltaX / sensitivity); // 计算该换多少张图
    if (frameChange !== 0) {
      let newFrameIndex = currentFrameIndex.value + frameChange; // 新帧索引 = 当前索引 + 变化量

      // 处理"循环":超过最后一张?回到第一张!
      while (newFrameIndex < 0) newFrameIndex += totalFrames;
      while (newFrameIndex >= totalFrames) newFrameIndex -= totalFrames;

      FrameUpdate(newFrameIndex); // 调用更新函数
      lastX.value = clientX; // 更新"上一次位置"为当前位置
    }
  }
  event.preventDefault(); // 阻止默认行为
};

​举个栗子🌰​​:
假设当前是第20张图("爪子扒拉罐子"),用户向右拖动了15px(deltaX=15),灵敏度是5。
frameChange = Math.floor(15/5) = 3 → 小棕熊要往后翻3张图(到第23张"歪头盯蜂蜜")。
如果用户向左拖动了10px(deltaX=-10),frameChange = Math.floor(-10/5) = -2 → 小棕熊要往前翻2张图(到第18张"抱蜂蜜发呆")。

(4)FrameUpdate:用requestAnimationFrame"优雅换图"

直接频繁修改currentFrameIndex会导致页面卡顿,所以Vue用了requestAnimationFrame——这是浏览器的"动画专用VIP通道",能保证动画流畅不丢帧。

const FrameUpdate = newFrameIndex => {
  if (pendingUpdate.value) return; // 如果有更新任务正在排队,直接溜了~
  pendingUpdate.value = true; // 标记"有待办任务"
  animationFrameId.value = requestAnimationFrame(() => {
    currentFrameIndex.value = newFrameIndex; // 正式更新帧索引
    updateFrame(); // 根据新索引找对应的图片路径
    pendingUpdate.value = false; // 任务完成,清空标记
  });
};

(5)updateFrame:给小棕熊"换动作包"

最后一步是根据当前帧索引,拼接出正确的图片路径。比如索引是0(对应第1张),路径就是../assets/img/frame/images/frame-1.gif

const updateFrame = () => {
  const frameNumber = currentFrameIndex.value + 1; // 索引0→第1张,索引1→第2张...
  currentFrame.value = `../assets/img/frame/images/frame-${frameNumber}.gif`; // 拼接路径
};

三、实际应用:小棕熊的"职场生涯"

这个组件能干嘛?答案是:​​任何需要"手动查看细节"的场景​​。

3.1 电商毛绒玩具展示

比如卖小棕熊玩偶的网店,用户可以用鼠标拖动查看玩偶的不同角度——比静态图片更直观,比3D建模更轻量(49张GIF也就几百KB)。

3.2 自然课动物行为演示

教动物行为学时,用这个组件展示小棕熊的真实动作(从"抱蜂蜜发呆"到"歪头盯蜂蜜"再到"爪子扒拉罐子")——学生拖动就能"控制"教学进度,比看视频更互动。

3.3 互动小游戏

做一个"黑熊探险"小游戏,用户拖动小棕熊完成指定动作(比如"歪头"找到隐藏蜂蜜,"晃爪子"推开石头)——用这个组件实现核心交互,成本比用Unity低得多。


四、幽默小剧场:小棕熊的"内心OS"

  • ​用户拖得太快​​:
    小棕熊OS:"哇!这是要带我去参加蜂蜜马拉松吗?等等我,图片还没加载完——啊!晕帧了!蜂蜜罐要掉了!"(实际:帧索引跳跃导致图片切换过快,可能模糊)
  • ​用户拖得太慢​​:
    小棕熊OS:"嗯?这是要欣赏我的绒毛吗?慢慢来,我连爪子上的蜂蜜渍都能给你看清楚~"(实际:灵敏度低,需要拖动更远才会换图)
  • ​用户突然松手​​:
    小棕熊OS:"哎?刚才那人跑哪去了?不管了,我继续保持这个角度——反正他还会回来摸我耳朵的!"(实际:拖动结束,小棕熊停在最后一帧)

五、总结:技术是工具,趣味是灵魂

这个小程序熊组件的核心其实很简单:​​用序列帧模拟动作变化,用事件监听实现交互,用性能优化保证流畅​​。但它告诉我们一个道理:

前端技术的魅力,不在于用了多复杂的框架,而在于能否用最简单的方式解决用户的需求——哪怕只是让一只小棕熊"听话"地晃爪子。

(最后温馨提示:如果你的小熊图片路径不对,记得检查frameNumber的计算——毕竟,小棕熊可不会自己"长脚"去找图片!)
1750301916944.gif


​参考资料​​:

  • Vue 3官方文档:@vue/runtime-core的事件处理
  • 序列帧动画原理:《计算机图形学基础》
  • 触摸事件与鼠标事件的兼容处理:MDN Web Docs
  • 图片下载地址[转csdn的@Zing22中的图片例子](项目首页 - frame-animation - GitCode)