这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战
前言
科学研究能破除迷信,
因为它鼓励人们根据因果关系来思考和观察事物。
——爱因斯坦
介绍
本期我将给大家讲解如何用Canvas API实现3D展示功能,或许我们经常会在一些商城中手机等产品会有这样效果,滑动屏幕,一个3D手机会旋转起来,其实很多情况都是一张张2D图片拼接而成,当然也有使用3D模型的。今天我们主角不是手机而是我童年最崇拜的一个角色——研究员,让他在屏幕中通过输入设备旋转起来。
大家先康康效果吧:
我们将会从基本结构,角色绘制,事件绑定等方面去讲解,来,这就出发~
正文
1.基本结构
<canvas id="canvas"></canvas>
<script type="module" src="./app.js"></script>
/*app.js*/
class Application {
constructor() {
this.canvas = null; // 画布
this.ctx = null; // 环境
this.w = 0; // 画布宽
this.h = 0; // 画布高
this.textures = new Map(); // 纹理集
this.spriteData = new Map(); // 精灵数据
this.roleIndex = 0; // 当前角色图片
this.isActive = false; // 输入设备是否按下
this.startX = 0; // 转动x轴起始点
this.init();
}
init() {
// 初始化
this.canvas = document.getElementById("canvas");
this.ctx = this.canvas.getContext("2d");
window.addEventListener("resize", this.reset.bind(this));
this.reset();
for (let i = 0; i < 36; i++) {
this.textures.set(i, `../assets/${i + 1}.webp`);
}
this.load().then(this.render.bind(this));
}
load() {
// 加载纹理
const { textures, spriteData } = this;
let n = 0;
return new Promise((resolve, reject) => {
if (textures.size == 0) resolve();
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);
}
})
}
reset() {
// 调屏重新获取宽高
this.w = this.canvas.width = this.ctx.width = window.innerWidth;
this.h = this.canvas.height = this.ctx.height = window.innerHeight;
}
render() {
// 渲染
this.step();
}
drawRole() {
// 绘制角色
}
step(delta) {
// 重绘
const { w, h, ctx } = this;
requestAnimationFrame(this.step.bind(this));
ctx.clearRect(0, 0, w, h);
this.drawRole();
}
}
window.onload = new Application();
我们期望的是加载36张图片,如下图:
其实我们的3D研究员就是这一张张图片拼凑而成,每一张图片代表一个角度,障眼法去实现的3D效果。
我们先定义出textures数据结构为字典,先在初始化当中存储起来,然后通过load方法去逐张加载,生成spriteData精灵容器,也是一个字典形式,我们后面绘制3D研究员就会从spriteData取出对应的图片,不断的绘制。
2.角色绘制
drawRole() {
const { ctx, w, h, spriteData, roleIndex } = this;
let img = spriteData.get(roleIndex);
ctx.save();
ctx.translate(w / 2 - img.width / 4, h / 2 - img.height / 4);
ctx.scale(.5, .5);
ctx.drawImage(img, 0, 0);
ctx.restore();
}
就跟上面说的一样,我们spriteData取出对应的图片,因为默认是0所以是第一张图片是正脸。绘制的时候也别忘了,绘制到画布中间,本身图片也挺大就缩放一半然后放到画布中央吧。这里可以看出来我们每次的绘制只要改变全局的roleIndex就可以绘制不同角度的研究员图片了。
3.事件绑定
大家应该不难猜到有哪些事件,无非是按下,移动,抬起,其中移动事件改变roleIndex值就完了。
但是,之前我们还要做一件事就是判断,是pc端事件还是移动端事件。
isMobile() {
let mobileArry = [
"iPhone",
"iPad",
"Android",
"Windows Phone",
"BB10; Touch",
"BB10; Touch",
"PlayBook",
"Nokia"
];
let ua = navigator.userAgent;
let res = mobileArry.filter(arr => {
return ua.indexOf(arr) > 0;
});
return res.length > 0;
}
render() {
this.step();
if (this.isMobile()) {
this.canvas.addEventListener("touchstart", this.touchStart.bind(this));
this.canvas.addEventListener("touchmove", this.touchMove.bind(this), false);
this.canvas.addEventListener("touchend", this.touchEnd.bind(this), false);
} else {
this.canvas.addEventListener("mousedown", this.touchStart.bind(this));
this.canvas.addEventListener("mousemove", this.touchMove.bind(this), false);
this.canvas.addEventListener("mouseup", this.touchEnd.bind(this), false);
this.canvas.addEventListener("mouseover", this.touchEnd.bind(this), false);
}
}
这是一个简易判断是否是手机端的事件。我们将会通过他在render函数里去绑定按下,移动,抬起,移出等事件。接下来,我们在做相应事件的处理。
touchStart(e) {
if (e.changedTouches) e = e.changedTouches[0];
this.startX = e.clientX;
this.isActive = true;
}
touchMove(e) {
if(!this.isActive) return;
if (e.changedTouches) e = e.changedTouches[0];
if (this.startX > e.clientX) {
this.roleIndex--;
}
if (this.startX < e.clientX) {
this.roleIndex++;
}
if (this.roleIndex < 0) this.roleIndex += 36;
this.roleIndex %= 36;
this.startX = e.clientX;
}
touchEnd() {
this.isActive = false;
}
我们每次按下和移动都会记录一下当前点,就是为了与下一次做一个对比,来改变图片的roleIndex,使其利用障眼法去让我们感觉图片根据输入设备在3D转动,其实仅仅是换了图片。当然图片一共36张,要做一些边界判断,然后就大功告成了。
结语
我们通过这种方式可以不需要引入3D模型就可以展示3D物体的转动,这个模型差不多是3M,而且我们这36张图片在未压缩的状态是500K,甚至我们还可以合并减少请求次数,然后再压缩,相信加载速度可以飞快的。