说图形模块化之前,先回顾下我们之前画的图形,那是一个多边形,虽然没有闭合,但这不重要。
接下来,咱们就将这个图形封装为一个类对象 Poly
Poly 对象是对路径的封装,那我们就可以从两方面来考虑:图形、样式。
Poly 对象是对路径的封装,我们可以从两方面来考虑:
- 图形:路径可以绘制的所有图形,可以是一个图形,也可以是多个图形,只要都在一个路径集合里就行;
- 样式:路径该有的所有样式了。
接下来我们看一下 Poly 对象的默认属性:
const defAttr={
crtPath:crtLinePath,
vertices:[],
close:false,
fill:false,
stroke:false,
shadow:false,
fillStyle:'#000',
strokeStyle:'#000',
lineWidth:1,
lineDash:[],
lineDashOffset:0,
lineCap:'butt',
lineJoin:'miter',
miterLimit:10,
shadowColor:'rgba(0,0,0,0)',
shadowBlur:0,
shadowOffsetX:0,
shadowOffsetY:0,
position:new Vector2(0,0),
rotation:0,
scale:new Vector2(1,1),
};
详细解释一下这些属性:
crtPath 是建立路径的方法,默认是给了一个绘制多边形的方法,此方法也可以被覆盖。
/* 绘制多边形 */
function crtLinePath(ctx){const {vertices}=this;
/* 连点成线 */
ctx.beginPath();
ctx.moveTo(vertices[0].x,vertices[0].y);
const len=vertices.length;
for(let i=1;i<len;i++){ctx.lineTo(vertices[i].x,vertices[i].y);
}
}
vertices 是多边形的顶点集合。
对于其它图形的相关属性,我没有写,以后需要了可以再去扩展,比如 arc 的圆心位、半径、起始弧度、结束弧度等等。
绘图方法相关的相关的属性:
- close:否闭合路径
- fill:否以填充方式绘制图形
- strtoke:否以描边方式绘制图形
- shadow:否给图像添加投影
样式相关的属性:fillStyle 填充样式、strokeStyle 描边样式…… 这些样式名称和 canvas 里的样式是一样的,我就不消多说了。
变换相关属性:和 canvas 里的变换是一样的,分别是位移、旋转、缩放。
Poly 的方法:
- draw(ctx):绘图方法
- checkPointInPath(ctx,{x,y}):检测点位是否在路径中
整体代码:
import Vector2 from "./Vector2.js";
/* 多边形默认属性 */
const defAttr={
crtPath:crtLinePath,
vertices:[],
close:false,
fill:false,
stroke:false,
shadow:false,
fillStyle:'#000',
strokeStyle:'#000',
lineWidth:1,
lineDash:[],
lineDashOffset:0,
lineCap:'butt',
lineJoin:'miter',
miterLimit:10,
shadowColor:'rgba(0,0,0,0)',
shadowBlur:0,
shadowOffsetX:0,
shadowOffsetY:0,
scale:new Vector2(1,1),
position:new Vector2(0,0),
rotation:0,
};
/* 绘制多边形 */
function crtLinePath(ctx){const {vertices}=this;
/* 连点成线 */
ctx.beginPath();
ctx.moveTo(vertices[0].x,vertices[0].y);
const len=vertices.length;
for(let i=1;i<len;i++){ctx.lineTo(vertices[i].x,vertices[i].y);
}
}
/*Poly 多边形 */
export default class Poly{constructor(param={}){Object.assign(this,defAttr,param);
}
draw(ctx){
const {
shadow, shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY,
stroke, close, strokeStyle, lineWidth, lineCap, lineJoin, miterLimit,lineDash,lineDashOffset,
fill, fillStyle,
scale,position,rotation
}=this;
ctx.save();
/* 投影 */
if(shadow){
ctx.shadowColor=shadowColor;
ctx.shadowBlur=shadowBlur;
ctx.shadowOffsetX=shadowOffsetX;
ctx.shadowOffsetY=shadowOffsetY;
}
/* 变换 */
ctx.translate(position.x,position.y);
ctx.rotate(rotation);
ctx.scale(scale.x,scale.y);
/* 建立路径 */
this.crtPath(ctx);
/* 描边 */
if(stroke){
ctx.strokeStyle=strokeStyle;
ctx.lineWidth=lineWidth;
ctx.lineCap=lineCap;
ctx.lineJoin=lineJoin;
ctx.miterLimit=miterLimit;
ctx.lineDashOffset=lineDashOffset;
ctx.setLineDash(lineDash);
close&&ctx.closePath();
ctx.stroke();}
/* 填充 */
if(fill){
ctx.fillStyle=fillStyle;
ctx.fill();}
ctx.restore();}
checkPointInPath(ctx,{x,y}){this.crtPath(ctx);
const bool=ctx.isPointInPath(x,y);
}
}
注意:Poly 对象中,我对其点位的定义使用了一个 Vector2 对象。
Vector2 是一个二维向量对象,它存储了基本的 x、y 位置信息,封装了点位的运算方法,比如加、减、乘、除等。
这里我先不对 Vector2 对象做详细解释,我们先知道它表示一个 {x,y} 点位即可,后面我们用到了它的哪个功能,再解释哪个功能。
Poly 对象建立完成后,咱们就画个三角形试试。
实例化 Poly 对象:
const poly=new Poly({
stroke:true,
close:true,
vertices:[new Vector2(50,50),
new Vector2(450,50),
new Vector2(250,250),
]
});
poly.draw(ctx);
效果:
为三角形添加划入划出效果:
let hover=false;
canvas.addEventListener('mousemove',mousemoveFn);
function mousemoveFn(event){const mousePos=getMousePos(event);
poly.crtPath(ctx);
const bool=ctx.isPointInPath(mousePos.x,mousePos.y);
if(hover!==bool){
poly.fill=bool;
ctx.clearRect(0,0,canvas.width,canvas.height);
poly.draw(ctx);
hover=bool;
}
}
鼠标选择逻辑:
1. 在事件外声明 hover 变量,存储鼠标划入划出状态。
2. 用 canvas 监听鼠标移动事件,获取鼠标在 canvas 中的位置
3. 使用 poly.crtPath(ctx) 方法建立路径
4. 使用 isPointInPath() 判断鼠标点位是否在路径中
5. 鼠标的选择状态发生了改变,让图形的填充样式也做相应的改变,并绘图。
鼠标划入效果:
把 svg 多边形画到 canvas 里
用 Poly 对象,我们还可以基于 svg 里的 polygon 数据绘图并选择。
接下来我用 Poly 对象画一下那座酷似大象的山。
在 svg 加载成功后,提取 svg 里的 polygon 的顶点,然后将其放到 Poly 的实例对象的 vertices 集合里。
window.onload = function() {const dom = embed.getSVGDocument();
const mount = dom.querySelector('#mount');
backImg = dom.querySelector('#back');
ctx.drawImage(backImg,0,0);
poly.vertices=parsePoints(mount);
poly.draw(ctx);
/* 鼠标移动 */
canvas.addEventListener('mousemove',mousemoveFn);
};
parsePoints(mount) 解析的就是下面 polygon 标签的 points 属性
<polygon id="mount" fill-rule="evenodd" clip-rule="evenodd" fill="none" stroke="#080102" stroke-miterlimit="10" points="
211.7,260.8 234.6,236.6 241.2,190.3 245.6,165.2 255.7,145.4 309.5,95.2 358.4,74.9 381.7,115.9 388.8,130.4 385.7,137.9
398,174.5 406.4,176.2 433.3,205.3 443.8,236.6 468.9,263 288.8,264.8 294.5,239.2 276,243.6 265.9,262.6 "/>
parsePoints(mount) 函数:
function parsePoints(dom){const points=[];
let pointsAttr=dom.getAttribute('points').split(' ');
for(let ele of pointsAttr){if(ele){const arr=ele.split(',');
const [x,y]=[Math.round(arr[0]),
Math.round(arr[1]),
];
points.push(new Vector2(x,y));
}
}
return points;
}
页面效果:
绘制其它图形
重写 Poly 对象的 crtPath() 方法,我们还可以绘制除多边形之外的其它图形,比如用两个三次贝塞尔画一颗爱心:
const poly=new Poly({position:new Vector2(300,400),
stroke:true,
close:true,
crtPath:function(ctx){ctx.beginPath();
ctx.moveTo(0,0);
ctx.bezierCurveTo(-200,-50,-180,-300,0,-200);
ctx.bezierCurveTo(180,-300,200,-50,0,0);
}
});
poly.draw(ctx);
这里其实还存在了一个问题,那就是图形通过变换属性发生了位移、旋转或缩放后,使用鼠标相对于 canvas 画布的点位就无法再对图形做出正确选择。
对于这个问题存在的原因和解决方式,咱们下章详解:图形选择 - 物质不易
源码地址