这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
前言
生命中真正重要的不是你遭遇了什么,而是你记住了哪些事,又是如何铭记的。
——《百年孤独》
介绍
你曾经紧跟过一个人的身后么,无论他走都哪儿你就跟到哪。
今天就让你体会到这种被追逐的感觉,翠花!上动图!!
本次开发的是一个精灵动画跟随鼠标移动和转向的Canvas场景。
具体主要做的是,获取图片资源,小海龟动画,获取鼠标坐标计算方向和位置。大体会经历以上这几个步骤,属于我们自己形影不离的小精灵就这么简单的做好了~
开发
1.基础结构
<style>
* {
padding: 0;
margin: 0;
}
html,
body {
width: 100%;
height: 100vh;
overflow: hidden;
}
#canvas {
width: 100%;
height: 100%;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script type="module" src="./app.js"></script>
</body>
/*app.js*/
import Pet from "./js/pet.js"
class Application {
constructor() {
this.canvas = null; // 画布
this.ctx = null; // 环境
this.w = 0; // 画布宽
this.h = 0; // 画布高
this.offsetX = 0 // 鼠标x轴坐标
this.offsetY = 0; // 鼠标y轴坐标
this.pet = null; // 当前的宠物
this.textures = new Map(); // 纹理集
this.spriteData = new Map(); // 精灵数据
this.init();
}
init() {
// 初始化
}
load() {
// 加载纹理
}
render() {
// 主渲染
}
drawBackground() {
// 绘制背景图
}
drawPet(dt) {
// 绘制精灵
}
step(delta) {
// 重绘
}
reset() {
// 屏幕改变事件
}
}
window.onload = new Application();
/* pet.js */
class Pet {
constructor(args) {
return this;
}
render(ctx) {}
draw() {}
update(dt) {}
}
export default Pet;
这里我们把基础结构先写出来,利用module模式来导入模块。我们期望是在实例化后自动调用初始化,接下来初始化才会获取DOM,加载纹理集,绑定事件的操作。
2.初始化
init() {
this.canvas = document.getElementById("canvas");
this.ctx = canvas.getContext("2d");
window.addEventListener("resize", this.reset.bind(this))
this.reset();
this.canvas.addEventListener("mousemove", e => {
this.offsetX = e.offsetX;
this.offsetY = e.offsetY;
})
this.textures.set("bg", "../assets/bg.png");
this.textures.set("sea", "../assets/sea.png");
this.load().then(this.render.bind(this));
}
reset() {
this.w = this.canvas.width = window.innerWidth;
this.h = this.canvas.height = window.innerHeight;
}
在初始化我们拿到了画布,然后绑定了监听屏幕变化的事件,同时,执行一下,获取并调整了画布大小与屏幕等大。初始化里我们还监听了鼠标事件拿到他的坐标位置。
最关键的是我们期望是要用Map结构去设置纹理,然后逐个加载并收集,整个加载完毕后再进入主渲染。
3.加载事件
load() {
const { textures, spriteData } = this;
let n = 0;
return new Promise((resolve, reject) => {
for (const key of textures.keys()) {
let _img = new Image();
spriteData.set(key, _img);
_img.onload = () => {
if (++n == textures.size)
resolve();
}
_img.src = textures.get(key);
}
})
}
我们找一个数量标记n来记录当前图片加载数量。利用for..of事件去遍历当前的纹理集,生成Img后塞进spriteData来存储。当所有纹理加载完毕后立即resolve。
4.渲染
render() {
const { w, h, spriteData, ctx } = this;
this.pet = new Pet({
img: spriteData.get("sea"),
x: w / 2,
y: h / 2,
offsetMaxY: 6,
rows: [1, 10],
speed:3
}).render(ctx);
this.offsetX = this.pet.x;
this.offsetY = this.pet.y;
this.step();
}
drawPet(dt) {
const { pet } = this;
if(!pet) return;
pet.update(dt);
}
step(delta) {
const { w, h, ctx } = this;
requestAnimationFrame(this.step.bind(this));
ctx.clearRect(0, 0, w, h);
this.drawBackground();
this.drawPet(delta);
}
这里我们就必须实例化一个宠物了,将宠物所需加载好的纹理传过去,然后定义当前坐标,行动速度,还有当前纹理配置信息,不要着急我们绘制小海龟的时候会有说明。随后,我们就进入step事件不断绘制。
5.绘制背景
drawBackground() {
const { w, h, spriteData, ctx } = this;
let img = spriteData.get("bg");
let { width, height } = img;
let _w, _h;
if (width / height * h > w) {
_w = width / height * h;
_h = h
} else {
_w = w;
_h = height / width * w;
}
ctx.save();
ctx.drawImage(img, w / 2 - _w / 2, h / 2 - _h / 2, _w, _h);
ctx.restore();
}
这里非常简单就是绘制刚才加载好的背景图,然后判断他的尺寸使之永远不比屏幕小且不变形。这么大气~~
6.宠物类逻辑
我们接下来来到重中之重了,如何让小海龟显示出来,怎么动起来全看这里了!
class Pet {
constructor(args) {
this.x = 0; // x轴坐标
this.y = 0; // y轴坐标
this.rows = [1, 1]; // 纹理图片对应位置数量
this.img = ""; // 纹理图片
this.rotation = 0; // 方向
this.scale = 1; // 大小
this.offsetY = 0; // 当前纹理图片y轴位置
this.offsetX = 0; // 当前纹理图片x轴位置
this.offsetMaxX = 1; // 纹理图片x轴位置限制
this.offsetMaxY = 1; // 纹理图片y轴位置限制
this.speed = 3; // 移动速度
Object.assign(this, args);
return this;
}
render(ctx) {
this.ctx = ctx;
this.width = this.img.width || 0;
this.height = this.img.height || 0;
this.w = this.img.width / this.rows[0];
this.h = this.img.height / this.rows[1];
this.draw();
return this;
}
draw() {
let { x, y, ctx, img, w, h, rotation, scale, offsetX, offsetY } = this;
if (!ctx || !img) return;
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.rotate(rotation);
ctx.drawImage(img, w * offsetX, h * offsetY, w, h, -w / 2, -h / 2, w, h);
ctx.restore();
}
update(dt) {
this.draw();
let _dt = Math.floor(dt);
_dt %= 7;
if (_dt == 0) {
this.offsetY++;
this.offsetY %= this.offsetMaxY;
}
}
}
export default Pet;
这里特别说明一些,因为从网上找的图片素材所以难免不尽人意。本次找的小海龟的素材因为后面动作有个摆头效果这里我们不想要要过滤掉,为了简单我们加了一个offsetMaxY来限制y的那几个动作的出现。
draw方法很简单,根据相应位置绘制图片,这里不多做赘述。至于,update方法,我们回头会在不断绘制种去调用来绘制他。跟7取余数是限制一下他的动画频率,不要让他过快。
现在我们大致就可以看到当前界面的样子了。
只是他不跟你走,真是只傻海龟。没办法,套路一下让他乖乖就范~~
7.宠物转向和移动
drawPet(dt) {
const { offsetX, offsetY, pet } = this;
if(!pet) return;
let dx = offsetX - pet.x;
let dy = offsetY - pet.y;
let angle = Math.atan2(dy, dx);
if ((dx < 10 && dx > -10) && (dy < 10 && dy > -10)) {
// 进入范围
}
else {
let vx = pet.speed * Math.cos(angle);
let vy = pet.speed * Math.sin(angle);
pet.x += vx;
pet.y += vy;
}
pet.rotation = angle;
pet.update(dt);
}
之前我们把写了的drawPet方法再进行扩展。这里要拿到鼠标坐标的位置做差。然后利用atan2去拿到他对应x轴到鼠标点的弧度,然后赋值到宠物的方向上,这样他就会跟你的鼠标转向了。
至于,怎么让他移动是计算他的vx和vy作为x轴和y轴的速度。利用三角函数cos和sin算出他们,再用speed作为系数去调整他的移动快慢。原理就是每次绘制都会往鼠标点加一定量的位移,直到达到鼠标点。但是我们要注意的是,鼠标点只是很小的一个点,很容易出现增加量溢出,动画位置反复闪动的bug。所以我们要设置一个范围,到达这个范围再不让他改变位移。
完成这一步差不多我们就完成了整个小项目了,怎么样,还有趣么,演示地址
拓展与延伸
我们可以利用这些知识制造出更有意思的场景,比如说投喂食物,宠物会吃而长大,或者做出表情。或者做些闯关类的任务也可以。再或者把整个网站首页改造成水世界,把小海龟当初你的鼠标来进入子页面。。
交互动画在前端还是蛮有乐趣的,如果天天写管理系统和商城app就未免太过枯燥。让生活不再孤单,做点新奇的,一起找点乐子吧。。