👀菜鸟前端摸爬滚打探索Canvas引擎要怎么实现(二)

625 阅读5分钟

个人博客:<Encaik学前端/> (encaik.gitee.io)

本篇重点

在上次做完初始的小结之后,我打算对引擎的基础进行一些完善。最基本的重新渲染以支持动画,或者实现一个画布网格背景,这都是我想要加入的,但是在这过程中也遇到了很多问题,所以我想把这个过程中遇到的问题和解决方式都记录下来。

动画支持

这块我最开始的想法是,写一个渲染器,然后通过requestAnimationFrame来做到持续刷新画布,这样我就可以做到数据更新后画布也更新,实现一个动画的效果。关键代码如下:

renderer(){
    this._engine.canvas.update(); // 画布更新即清除画布所有内容重新绘制
    this._lastTime = this._nowTime;
    this._nowTime = performance.now();
    this._fps = Math.round(1000/(this._nowTime-this._lastTime));
    window.requestAnimationFrame(()=>{
        this.renderer(); // 重复自调用实现画布更新
    });
}
graph TD
render  --> requestAnimationFrame
requestAnimationFrame --> render
render --> update 

这么完成以后效果是还行的,然后输出fps,基本是60左右浮动,我当时觉得还行,并没有察觉出什么问题。直到我做了下一步操作,鼠标拖动图形之后,页面的卡顿超乎我的想象。最直观的感受就是,我拖动图形时,图形并不是紧跟鼠标的,而是以跟随的效果,完成拖动的路径。

然后经过我与同事的讨论,我发现了两个导致这个情况出现的问题,这里先谈渲染导致的。

我同事的意见是,既然我这个是个canvas引擎,那么渲染的时机为什么是raf来决定,而不是引擎来决定。听完这句话的我,醍醐灌顶,反应过来了。我又不是在做动画,我做的是动画支持,自然不需要用一个定时渲染来做画布的更新,我应该是在引擎触发更新时更新画布。

然后我就对鼠标拖动部分的代码做了修改,当图形有事件触发或者数据变更时,触发更新方法,更新画布。关键代码如下:

/* render.ts */
/* 这里渲染器去掉了raf,改成了单次渲染,并计算fps */
renderer(){
    this._engine.canvas.update();
    this._lastTime = this._nowTime;
    this._nowTime = performance.now();
    this._fps = Math.round(1000/(this._nowTime-this._lastTime));
}

/* behavior.ts */
/* 当拖动图形后,通过引擎update方法触发画布更新 */
const offset = [0,0];
if(this._isDrag){
    if(this._dragBeforePointer){
        offset[0] = pointer.x-this._dragBeforePointer.x;
        offset[1] = pointer.y-this._dragBeforePointer.y;
    }
    this._dragBeforePointer = pointer;
    if(this._dragShape){
        this._dragShape.center.x += offset[0];
        this._dragShape.center.y += offset[1];
        this._engine.dispatcher.dispatch("shapedrag",{
            event:e.data.event,
            shape:this._dragShape
        });
        this._engine.update();
        return;
    }else{
        this._engine.canvas.translate(offset[0],offset[1]);
        this._engine.update();
        return;
    }  
}
graph TD
数据变更或状态变更 --> render --> update

改完后再也没有跟随的情况发生了,鼠标移动则图形移动。输出fps,可以看到在鼠标拖动图形时,fps可以达到一千左右,鉴于鼠标的刷新率最高就是一千多,我感觉这个fps最高可能和鼠标刷新率有关。

图形拖动

图形拖动设计导致的问题,也是上面提到的图形跟随的原因之一。

这个问题的成因在于我设计图形拖动这个功能时,想要即时算出move事件触发时,鼠标位置的所有图形。因为每个图形的class我都会添加isContains方法来判断一个点是否在图形内,所以我只需要获取场景中所有图形调用该方法,然后返回一个数组即可。

getContainsShapes(point:Point){
    const shapes = [];
    this.entityList.forEach(entity=>{
        if(entity.isContains(point)){
            shapes.push(entity);
        }
    });
    return shapes;
}
onMouseDown(e:Event){
    const pointer = new Point(e.data.event.x,e.data.event.y);
    this._isDrag=true;
    this._dragBeforePointer = null;
    this._dragShape = this._engine.scene.getContainsShapes(pointer);
}
graph TD
1[按下鼠标] --> 2[拖动状态改为true] --> 4[判断当前拖动状态]
3[移动鼠标] --> 4[判断当前拖动状态] --true--> 5[获取当前鼠标下所有图形] --> 6[将鼠标位置赋值给图形]

但是按这个方法实现以后,发现性能比较一般,所以思考以后换为另一种实现方式。即常规的,mousedown事件直接缓存点击到的位置获取到的所有图形,然后每次移动都赋值新的鼠标位置。

graph TD
1[按下鼠标] --> 2[拖动状态改为true] --> 3[获取当前鼠标下所有图形并缓存]
4[移动鼠标] --> 5[判断当前拖动状态] --true--> 6[将鼠标位置赋值给缓存图形]

上一个其实还没优化到位,因为只是不需要在拖动的时候持续获取图形列表,但是获取图形列表这个操作本就不是很舒服,所以下一步就是直接重构整个画布的图形拾取。

画布图形拾取

一开始图形的拾取是靠一个坐标点,然后判断是否在图形内计算,但是在开发过程中,我感觉到这样的方式对运行性能带来了极大的负荷,但是我又想不出好的解决方案。然后我开始查找别人是如何判断拾取图形的,最终在各种方案下,我决定学习再创建一个canvas,通过点击像素颜色识别图形的方案。

这个方案其实就是在画布上叠一层看不见的蒙版,没有图形的位置像素点是白色,有图形的位置则是各种颜色,理论上这种方法支持16777215个图形的辨识。

this._detectionCanvas = document.createElement("canvas");
this._detectionCtx = this._detectionCanvas.getContext("2d", { alpha: false });
// document.body.appendChild(this._detectionCanvas);
if (options.width) this._detectionCanvas.width= options.width;
if (options.height) this._detectionCanvas.height= options.height;
this._shapeMap = new Map();
this._colorMap = new Map();
public detectionShape(point:Point):Entity{
    const imgData = this._detectionCtx.getImageData(point.x,point.y,1,1);
    const color = [imgData.data[0],imgData.data[1],imgData.data[2]].join(",");
    if(this._shapeMap.has(color)){
        return this._shapeMap.get(color);
    }
}

在这里因为需要通过图形映射颜色,做到相同图形不会更换颜色导致拾取出现问题,又需要通过颜色映射图形,在拾取时直接找到图形对象,所以我需要一个双向映射的结构,但因为暂时没有什么更好的思路,所以只是使用了两个map来做到双向映射。

虚拟坐标系的建立

因为图形拖动实现以后,我又想实现画布的拖动,所以我搜索了canvas的api发现了画布变换方法,于是我打算使用这个方法来实现画布的变换。

public translate(x:number,y:number){
    this._ctx.translate(x,y);
    this._detection.translate(x,y);
}

但是在这么实现以后,问题出现了。因为画布更新需要清空画布重绘,所以我使用的是clearRect方法清空画布,在移动画布以后,该方法只会清除固定的那块区域,导致区域外的部分不会被清空。所以我需要一个虚拟坐标系,即画布不动,虚拟坐标系移动。

export interface CanvasTransform {
  translate:CanvasTranslate
}

export interface CanvasTranslate {
  x:number;
  y:number;
}
public translate(offsetX:number,offsetY:number){
    const x = this._transform.translate.x+offsetX;
    const y = this._transform.translate.y+offsetY;
    this._transform.translate = {x,y};
    this._detection.translate(x,y);
}

这部分目前只添加了平移操作,实际体验效果还不错,之后打算加入缩放和旋转,算是难度逐渐升高。

小结

下个阶段依旧是对项目基础功能的完善,以及图形的扩充,我也会在开发中继续学习,看到学到更多的东西。