Canvas小技巧之透视法

1,631 阅读3分钟

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

前言

如你总是盯着太阳,太阳也会使你双目失明的。——卢克莱修

介绍

本期我们会讲述一个Canvas做伪3D的一个核心原理——透视法,即它主要借助于近大远小的透视现象来表现物体的立体感。我们将会做一个简单的案例来分析一下,如果通过鼠标滚轮来实现透视法的近大远小。我们先康康效果吧:

VID_20211108_201851.gif

相信小伙伴会问这不就是通过鼠标滚轮控制图片放大缩小么,对,也不全对,想想如果有很多物体,每个物体都控制他的缩放比而且接近超过它也可以穿过这是一件麻烦事,我们需要一个更加科学的方案去控制。接下来,我们就通过一个案例去揭秘吧。

正文

1.基础界面

class Application {
    constructor() {
        this.canvas = null;            // 画布
        this.ctx = null;               // 环境
        this.w = 0;                    // 场景宽
        this.h = 0;                    // 场景高
        this.textures = new Map();     // 纹理集
        this.spriteData = new Map();   // 精灵数据
        this.alpha = 1;                // 透明度
        this.r = 300;                  // 太阳半径
        this.x = 0;                    // 太阳x轴坐标
        this.y = 0;                    // 太阳y轴坐标
        this.z = 0;                    // 太阳z轴坐标
        this.deep = 100;               // 视距
        this.init();
    }
    init() {
        // 初始化
        this.canvas = document.getElementById("canvas");
        this.ctx = this.canvas.getContext("2d");
        window.addEventListener("resize", this.reset.bind(this));
        this.reset();
        this.textures.set("sun", `../assets/sun.png`);
        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() {
        // 屏幕改变重新计算宽高与xy轴坐标
        this.w = this.canvas.width = this.ctx.width = window.innerWidth;
        this.h = this.canvas.height = this.ctx.height = window.innerHeight;
        this.x = this.w / 2;
        this.y = this.h / 2;
    }
    render() {
        // 主渲染
        this.draw();
        this.step();
        this.bindWheel();
    }
    draw(){
        // 绘制
        this.drawSun();
        this.drawBackground();
    }
    bindWheel(){
        // 绑定滚轮事件
    }
    drawSun(){
        // 绘制太阳
    }
    drawBackground(){
        // 绘制背景
    }
    step(delta) {
        // 重绘
        const { w, h, ctx } = this;
        requestAnimationFrame(this.step.bind(this));
        ctx.clearRect(0, 0, w, h);
        this.draw();
    }
}

window.onload = new Application();

我们还是那老几套,拿到画布给他一个2d环境,赋予他宽高,然后把用到的图片先加载下来,再把加载好的图片存入spriteData当做精灵随时取用。然后我们还没绘制实质性的东西,但是先把绘制结构写好,然后一帧帧重绘画面。在定义变量的时候大家应该发现了视距这个东西,也就是视点至对应点的距离,他的定义让整个2d世界一下有了空间感。

2.绘制背景色和太阳

drawSun(){
    const {w,h,ctx,spriteData,alpha,r,x,y,scale}= this;
    let img = spriteData.get("sun");
    ctx.save();
    ctx.translate(x, y);
    ctx.scale(scale,scale);
    ctx.globalAlpha = alpha;
    ctx.beginPath();
    ctx.drawImage(img, -r / 2, - r / 2, r, r);
    ctx.closePath();
    ctx.restore();
}
drawBackground(){
    const {w, h, ctx } = this;
    ctx.save();
    ctx.fillStyle = "#000"
    ctx.fillRect(0, 0, w, h);
    ctx.restore();
}

背景是绘制一个纯黑色的矩形,而太阳我们将会从刚才存入spriteData的图片找到它,绘制在屏幕中央,但别忘了他的大小scale和透明度alpha是通过外界可控制的,我们即将用他们来完成透视法的使用。

微信截图_20211108223625.png

3.滚轮事件

前面该铺垫的都铺垫完了,我们将在这个小事件里完成透视法的使用。

bindWheel(){
    canvas.addEventListener("wheel", e => {
        this.z += e.deltaY * 0.05;
        if (this.z > -this.deep) {
            this.alpha =1;
            this.scale = this.deep / (this.deep + this.z);
        }else{
            this.alpha = 0;
        }
    });
}

可以看到我们将通过滚轮事件对其z轴进行改变,而z轴将会影响到视距的比例,进一步改变其大小,而我们说这只的条件判断是如果z坐标在特定值内,太阳是可见的,当不在这个值则为穿过了他变为不可见,这样一来,我每次只需改变z这个值,就可以进一步实现其空间关系。

结语

不要看这个简单,其在canvas 2d模拟3d是必备的要素,十分的实用。比如,做3d粒子花园,都可以不用借助webgl,仅仅用它模拟z轴去完成。下面是很久之前的个人做的一个养猫的案例,就用了这个方法,在近处变大,远处变小,如果有同学问这个养猫的小游戏为啥不参加本次养猫活动,答案是我把源码丢了。。。

VID_20211108_230126.gif