WebGL Tutorial
and more

可拖曳的图形

撰写时间:2024-04-14

修订时间:2024-04-14

使用Path2D技术,实现了可在Canvas上拖曳的几何图形。

基类DraggableShape

基类DraggableShape代码:

import {grids} from './canvas-grids.js'; grids.setupCanvas('myCanvas'); let {canvas, ctx} = grids; let shapeHover = null; let shapeDragging = null; let isDragging = false; class DraggableShape { pathDataWrapper; color; hiColor; path2DObj; constructor(pathDataWrapper, color = 'CADETBLUE', hiColor = 'AQUAMARINE') { this.pathDataWrapper= pathDataWrapper; this.color = color; this.hiColor = hiColor; this.path2DObj = new Path2D(); this.doOnDefineShape(this.path2DObj, this.pathDataWrapper); } draw() { ctx.fillStyle = (shapeHover === this) ? this.hiColor : this.color; ctx.fill(this.path2DObj); } drag(x, y) { delete this.path2DObj; this.path2DObj = new Path2D(); this.doOnDragging(x, y, this.pathDataWrapper); this.doOnDefineShape(this.path2DObj, this.pathDataWrapper); } isPointInPath(x, y) { return ctx.isPointInPath(this.path2DObj, x * grids.scale, y * grids.scale); } // virtual methods doOnDragging(x, y, pathDataWrapper) { } // virtual methods doOnDefineShape(path2DObj, pathDataWrapper) { } }

使用了CanvasGrids提供背景网络,并提供了高分辨率设备的自适应。

作为基类,只提供了最核心的功能。包括类型为Path2Dpath2DObj,以及常规颜色color及高亮颜色hiColor

由于Path2D并未提供操控内部路径的方法,因此在响应用户拖曳行为时,先删除原来的path2DObj,再重新生成新的实例。

drag(x, y) { delete this.path2DObj; this.path2DObj = new Path2D(); this.doOnDragging(x, y, this.pathDataWrapper); this.doOnDefineShape(this.path2DObj, this.pathDataWrapper); }

由于经常删除并重新创建,因此上面代码delete this.path2DObj可让JavaScript引擎在必要时及时释放垃圾内存。

因各种图形有不尽相同的参数,为增加灵活性,doOnDefineShapedoOnDragging这两个方法被设计为纯虚类方法pure virtual method, C++术语),由所继承的子类自己实现。

子类DraggableCircle

现在,可定义子类了。先定义一个DraggableCircle类:

class DraggableCircle extends DraggableShape { constructor(pathDataWrapper) { super(pathDataWrapper); } doOnDragging(x, y, pathDataWrapper) { pathDataWrapper.x = x; pathDataWrapper.y = y; } doOnDefineShape(path2DObj, pathDataWrapper) { const {x, y, radius} = pathDataWrapper; path2DObj.arc(x, y, radius, 0, Math.PI * 2); } }

有此定义,客户端可这样调用:

let circle1 = new DraggableCircle({x: 100, y: 100, radius: 15});

对于在构造参数中所传入的{x: 100, y: 100, radius: 15},由doOnDefineShape方法制定如何利用它来绘制出一个图形的规则,在这里是绘制出一个圆形:

doOnDefineShape(path2DObj, pathDataWrapper) { path2DObj.arc(pathDataWrapper.x, pathDataWrapper.y, pathDataWrapper.radius, 0, Math.PI * 2); }

而当用户在Canvas上拖动该类图形时,由doOnDragging来定义如何根据鼠标坐标来更新其内部状态:

doOnDragging(x, y, pathDataWrapper) { pathDataWrapper.x = x; pathDataWrapper.y = y; }

这个子类的设置就完成了。

子类DraggableRect

下面再看如何实现可拖曳的矩形:

class DraggableRect extends DraggableShape { constructor(pathDataWrapper) { super(pathDataWrapper); } doOnDragging(x, y, pathDataWrapper) { pathDataWrapper.x = x - pathDataWrapper.width / 2; pathDataWrapper.y = y - pathDataWrapper.height / 2; } doOnDefineShape(path2DObj, pathDataWrapper) { const {x, y, width, height} = pathDataWrapper; path2DObj.rect(x, y, width, height); } } let rect1 = new DraggableRect({x: 100, y: 300, width: 100, height: 100});

同样的三部曲,传入矩形所特有的数据,利用该数据绘制矩形,定义在拖动时如何更新内部状态。

代码:

doOnDragging(x, y, pathDataWrapper) { pathDataWrapper.x = x - pathDataWrapper.width / 2; pathDataWrapper.y = y - pathDataWrapper.height / 2; }

可根据鼠标位置来自动居中该矩形。

因为如何绘制具体的图形完全由子类自己定义,因此,在需要时我们很容易实现诸如三角形、直线等不同图形的子类。

其他代码

function clear() { ctx.clearRect(-grids.PADDING, -grids.PADDING, canvas.width, canvas.height); } canvas.addEventListener('mousemove', (e) => { if (isDragging) { shapeDragging.drag(e.offsetX - grids.PADDING, e.offsetY - grids.PADDING); clear(); renderCanvas(); return; } for (let i = 0; i < sceneObjs.length; i++) { if (sceneObjs[i].isPointInPath(e.offsetX, e.offsetY)) { if (sceneObjs[i] === shapeHover) { // hover while moving break; } else { // enter hover shapeHover = sceneObjs[i]; sceneObjs[i].draw(); canvas.style.cursor = 'pointer'; break; } } else { if (sceneObjs[i] === shapeHover) { // moving out of shape shapeHover = null; sceneObjs[i].draw(); canvas.style.cursor = 'auto'; break; } } } }); canvas.addEventListener('mousedown', (e) => { if (!shapeHover) { return; } if (!isDragging) { shapeDragging = shapeHover; isDragging = true; canvas.style.cursor = 'move'; } }); canvas.addEventListener('mouseup', (e) => { if (isDragging) { shapeDragging = null; isDragging = false; canvas.style.cursor = 'pointer'; } }); function renderCanvas() { sceneObjs.grids.draw(); sceneObjs.forEach(obj => { obj.draw(); }); } let sceneObjs = []; sceneObjs.grids = grids; let circle1 = new DraggableCircle({x: 100, y: 100, radius: 15}); sceneObjs.push(circle1); let circle2 = new DraggableCircle({x: 200, y: 200, radius: 15}); sceneObjs.push(circle2); let rect1 = new DraggableRect({x: 100, y: 300, width: 100, height: 100}); sceneObjs.push(rect1); renderCanvas();

运行应用

因为CanvasGrids使用了缩放及平移变换,因此上面这些类也统一进行了处置。

Path2D来实现可拖曳的图形,优势是其isPointInPath可方便地判断鼠标是否悬停在哪一个独立的图形上,不便利的地方在于不方便操作其内部路径数据,且需额外考虑图形变换的问题。

而实现一个普通的可拖曳的应用,难点在于需同时跟踪较多的内部状态,尤其是当场景中的对象数量较多时。因此上面代码的条件语句看起来较多,但均是有必要的。

参考资源

  1. HTML5 Canvas Element