Canvas如何做个3D研究员

1,402 阅读3分钟

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

前言

科学研究能破除迷信,

因为它鼓励人们根据因果关系来思考和观察事物。

——爱因斯坦

介绍

本期我将给大家讲解如何用Canvas API实现3D展示功能,或许我们经常会在一些商城中手机等产品会有这样效果,滑动屏幕,一个3D手机会旋转起来,其实很多情况都是一张张2D图片拼接而成,当然也有使用3D模型的。今天我们主角不是手机而是我童年最崇拜的一个角色——研究员,让他在屏幕中通过输入设备旋转起来。

大家先康康效果吧:

VID_20211103_210342.gif

我们将会从基本结构,角色绘制,事件绑定等方面去讲解,来,这就出发~

正文

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张图片,如下图:

微信截图_20211103211306.png

其实我们的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就可以绘制不同角度的研究员图片了。

微信截图_20211103215003.png

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,甚至我们还可以合并减少请求次数,然后再压缩,相信加载速度可以飞快的。