引言
你在学three时是否苦于没有3d模型?但是业务又需要实现一个模型的旋转,组长啪的一扔,这里面是序列帧,立马给我实现伪3D的旋转起来!干不了不给你下播!可恶,没有模型,看着这50张的序列帧,想到我们的电影也是一帧一帧的图片组成的动画,连起来就相当于也是动起来啦!所以饿着肚子,吃掉了这50张图片。反手网上搜索例子,也是吃饱了。
一、核心概念:什么是"交互式序列帧动画"?
1.1 序列帧动画:会"变戏法"的图片集
序列帧动画(Sprite Animation)是一种经典的动画技术,原理简单粗暴:用多张连续的静态图片,通过快速切换形成动态效果。比如你小时候看的《黑猫警长》,里面的角色其实就是几张不同动作的小图片按顺序播放。
在我们的案例里,小棕熊有49张"GIF表情包"(frame-1.gif到frame-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会被触发。它的任务是:
- 记录当前拖动的起点坐标(
startX和lastX)。 - 关闭之前的动画帧更新(避免"手忙脚乱")。
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的计算——毕竟,小棕熊可不会自己"长脚"去找图片!)
参考资料:
- Vue 3官方文档:
@vue/runtime-core的事件处理 - 序列帧动画原理:《计算机图形学基础》
- 触摸事件与鼠标事件的兼容处理:MDN Web Docs
- 图片下载地址[转csdn的@Zing22中的图片例子](项目首页 - frame-animation - GitCode)