1个demo带你入门canvas

1,961 阅读4分钟

canvas画布是前端一个比较重要的能力,在MDN上看到有关canvas的API时,是否会感到枯燥?今天老弟就给各位带来了1个还说得过去的demo,话不多说,咱们一起来尝尝咸淡。

一、小球跟随鼠标移动

先来欣赏一下这段视频:

从上图我们发现,小球跟着我们的鼠标移动,并且鼠标点到的位置就是小球的中心点。想要实现这样的功能,我们可以将它抽象为下面图里的样子:

canvas2.png

是的,一个是画布(canvas)类,一个是小球(Ball)类。

canvas主要负责尺寸、执行绘画、事件监听。

Ball主要负责圆心坐标、半径、以及更新自己的位置。

接下来就是代码部分,我们先来完成canvas类的实现。

1.1、初始化canvas类属性

从上面的视频以及拆解的图里,我们会发现这个画布至少拥有以下几个属性:

  • width。画布的宽度
  • height。画布的高度
  • element。画布的标签元素
  • context。画布的渲染上下文
  • events。这个画布上的事件集合
  • setWidthAndHeight()。设置画布的大小
  • draw()。用于执行绘画的函数
  • addEventListener()。用于给canvas添加相应的监听函数

因此我们可以得出下面这段代码:

class Canvas {
    constructor(props){
        this.element = props.element;
        this.canvasContext = props.element.getContext('2d');
        this.events = [];
        this.width = props.width;
        this.height = props.height;
    }
}

1.2、设置canvas的大小

class Canvas {
    // 构造器
    constructor(props){
        this.element = props.element;
        this.canvasContext = props.element.getContext('2d');
        this.events = [];
        this.width = props.width;
        this.height = props.height;
        this.setWidthAndHeight(props.width, props.height);
    }
    // 设置canvas的宽度与高度
    setWidthAndHeight(width, height){
        this.element.setAttribute('width', width);
        this.element.setAttribute('height', height);
    }
}

在上面的代码里,我们手动实现了一个setWidthAndHeight这样的函数,并且当执行new操作的时候,自动这个函数,从而达到设置几何元素大小的作用。

1.3、绘画小球

我们的这个绘画功能应该只负责绘画,也就是说这个小球的位置坐标等信息应该通过传参的形式传入到我们的draw函数里

class Canvas {
    // 构造器
    constructor(props){
        this.element = props.element;
        this.canvasContext = props.element.getContext('2d');
        this.events = [];
        this.width = props.width;
        this.height = props.height;
        this.setWidthAndHeight(props.width, props.height);
    }
    // 设置canvas的宽度与高度
    setWidthAndHeight(width, height){
        this.element.setAttribute('width', width);
        this.element.setAttribute('height', height);
    }
    // 绘画小球
    draw(ball){
        this.canvasContext.beginPath();
        this.canvasContext.arc(ball.centerX, ball.centerY, ball.radius, 0, 2 * Math.PI, true);
        this.canvasContext.fillStyle = ball.background;
        this.canvasContext.fill();
        this.canvasContext.closePath();
    }
}

1.4、给canvas添加事件监听

根据我们的需求,小球随着鼠标移动,说明我们应该是在canvas标签上监听mouseover事件。我们再来思考一下,其实这个事件监听函数也应该保持纯粹。纯粹的意思就是,你不要在这个方法里去写业务逻辑。这个函数只负责添加相应的事件监听。

接下来我们实现一下这个addEventListener函数。

class Canvas {
    // 构造器
    constructor(props){
        this.element = props.element;
        this.canvasContext = props.element.getContext('2d');
        this.events = [];
        this.width = props.width;
        this.height = props.height;
        this.setWidthAndHeight(props.width, props.height);
    }
    // 设置canvas的宽度与高度
    setWidthAndHeight(width, height){
        this.element.setAttribute('width', width);
        this.element.setAttribute('height', height);
    }
    // 画物体
    draw(ball){
        this.canvasContext.beginPath();
        this.canvasContext.arc(ball.centerX, ball.centerY, ball.radius, 0, 2 * Math.PI, true);
        this.canvasContext.fillStyle = ball.background;
        this.canvasContext.fill();
        this.canvasContext.closePath();
    }
    // 添加监听器(eventName:要监听的事件,eventCallback:事件对应的处理函数)
    addEventListener(eventName, eventCallback){
        let finalEventName = eventName.toLocaleLowerCase();
        if (!this.events.filter(item => item === finalEventName)?.length > 0){
            this.events.push(finalEventName);
        }
        this.element['on' + finalEventName] = (target) => {
            eventCallback && eventCallback(target);
        }
    }
}

好啦,Canvas类的实现到这里先告一段落,我们来看看小球(Ball)类的实现。

1.5、Ball类的初始化

Ball这个类比较简单,4个属性+一个方法。

属性分别是:

  • centerX。小球的圆心横坐标X
  • centerY。小球的圆心纵坐标Y
  • background。小球的背景色
  • radius。小球的半径

方法是updateLocation函数。这个函数同样也是一个纯函数,只负责更新圆心坐标,更新的值也是由参数传递。

class Ball {
    constructor(props){
        this.centerX = props.centerX;
        this.centerY = props.centerY;
        this.radius = props.radius;
        this.background = props.background || 'orange';
    }

    // 更新小球的地理位置
    updateLocation(x, y){
        this.centerX = x;
        this.centerY = y;
    }
}

1.6、添加推动器

说的直白点,就是我们现在只有2个class类,但是无法实现想要的效果,现在来想想什么时机去触发draw方法。

根据上面的视频,我们知道需要在canvas标签上添加鼠标over事件,然后在over事件里来实时获取小球的位置信息,最后再触发draw方法。

当鼠标离开canvas画布后,需要将画布上的内容清除,不留痕迹。

这样一来,我们不仅要实现类似桥梁(bridge)的功能,还需要在canvas类上实现“画布清空”的功能。


class Canvas {
    // ...其他代码不动
    
    // 清空画布的功能
    clearCanvas(){
        this.canvasContext.clearRect(0, 0, this.width, this.height);
    }
}

// 画布对象
let canvas = new Canvas({
    element: document.querySelector('canvas'),
    width: 300,
    height: 300
});

// 小球对象
let ball = new Ball({
    centerX: 0,
    centerY: 0,
    radius: 30,
    background: 'orange'
});

// 给canvas标签添加mouseover事件监听
canvas.addEventListener(
    'mousemove',
    (target) => {
        canvasMouseOverEvent(target, canvas.element, ball);
    }
)

canvas.addEventListener(
    'mouseleave',
    (target) => {
        canvasMouseLeave(target, canvas);
    }
)

// 鼠标滑动事件
function canvasMouseOverEvent(target, canvasElement, ball){
    let pointX = target.offsetX;
    let pointY = target.offsetY;
    ball.updateLocation(pointX, pointY);
    canvas.draw(ball);
}

// 鼠标离开事件
function canvasMouseLeave(target, canvasInstance){
    canvasInstance.clearCanvas();
}

这样一来,我们便实现了大致的效果,如下:

我们似乎实现了当初的需求,只是为啥目前的表现跟“刮刮乐”差不多?因为下一次绘画的时候,没有将上次的绘画清空,所以才导致了现在的这个样子。

想要解决这个bug,只需在canvas类的draw方法里,加个清空功能就可以了。

class Canvas {
    // 画物体
    draw(ball){
        this.clearCanvas();
        this.canvasContext.beginPath();
        this.canvasContext.arc(ball.centerX, ball.centerY, ball.radius, 0, 2 * Math.PI, true);
        this.canvasContext.fillStyle = ball.background;
        this.canvasContext.fill();
        this.canvasContext.closePath();
    }
}

如此一来,这个bug就算解决了。

二、最后

好啦,本期内容到这里就告一段落了,希望这个demo能够帮助你了解一下canvas的相关API,如果能够帮到你,属实荣幸,那么我们下期再见啦,拜拜~~