本文已参与「新人创作礼」活动,一起开启掘金创作之路。 点击查看活动详情
写在前面
前面我们实现了一个简单的canvas库 100行代码写个canvas库 ,现在我们试一下这个库实际用起来怎么样,能不能满足常规的开发要求。流程图大家应该都见过用过,我们实是能不能整个简易版出来。
像这种
文字节点
由于前面我们实现的库已经实现了拖拽、点击这些交互功能,我们直接实现具体的业务类。 一个文字节点由一个文本,一个背景,一个删除按钮,四周边线中间的吸附点(连线的时候要用到)
class TextElement {
constructor(options) {
// 先初始化一个组合容器
this.container = new Container({
x: options.x,
y: options.y,
w: options.w,
h: options.h
});
// 内置一个粉色背景(这里当然可以用传参来自定义颜色)
this.bg = new Rect({
x: 0,
y: 0,
w: options.w,
h: options.h,
offsetX: 0,
offsetY: 0,
color: "pink"
})
// 内置一个删除按钮
this.icon = new Icon({
offsetX: options.w - 20,
offsetY: 0,
w: 20,
h: 20,
src: "./close"
}); // icon固定在容器右上角
this.icon.addEvent("click", (t) => {
this.container.destory();
});
// 文本
this.word = new Word({
text: options.text,
offsetX: 0,
offsetY: options.h / 2,
color: "blue"
}); // 文本垂直居中
// 把这几个元素都加到容器里
this.container.add(this.bg)
this.container.add(this.icon);
this.container.add(this.word);
return this.container
}
}
这样一个文字节点就准备好了,我们初始化几个看看效果
// 初始化一个800 * 700的舞台
let s2 = new Stage(document.getElementById("stage"));
// 在 (50, 50)的位置初始化一个200 * 50的文字板
let t1 = new TextElement({
text: "hello 解决1",
x: 350,
y: 250,
w: 200,
h: 50,
color: "blue",
parent: s2
});
s2.add(t1);
连线节点
连线的功能一度搞得我好头痛,因为连线的操作跟其他的节点操作完全不一样。 首先连线有两个端点都可以拖拽,连线还能拖拽到文本节点上,拖上去就要吸附住,接下来拖拽文本节点,线段也要跟着变,连线还能从节点上拖走,绘制连线的箭头,两个文本之间怎么绘制出一个最合适的连线(这个我暂时只解决了几个简单的相对位置)等等,所以最终连线节点我没有用组合容器Container实现,而是单独实现了一个类,具体功能设计在代码里都有注释。
class Connect {
// 连线的起止点,可以是文本节点
constructor(startPoint, endPoint) {
// 起点,Point其实就是一个实心圆,只不过封装了一下,响应了Stage的功能,有了拖拽和事件的能力
this.startPoint = new Point({
x: startPoint.x,
y: startPoint.y,
r: 5,
color: "green"
})
// 终点
this.endPoint = new Point({
x: endPoint.x,
y: endPoint.y,
r: 5,
color: "green"
})
// 把拖拽点放入children用来判断绘制
this.children.push(this.startPoint)
this.children.push(this.endPoint)
}
draw(ctx) {
// 重头戏
// 1 要判断起止点是否有吸附对象,有的话要更新位置
if(this.startBindTarget) {
this.startPoint.x = this.startBindTarget.x
this.startPoint.y = this.startBindTarget.y
}
if(this.endBindTarget) {
this.endPoint.x = this.endBindTarget.x
this.endPoint.y = this.endBindTarget.y
}
// 2 绘制线段,和最后的指向箭头,这里我自己根据起始点生成了一条路径(可以自行实现)
ctx.beginPath();
ctx.lineWidth = 3
ctx.strokeStyle = this.color;
// 绘制连线,箭头用倒数第二个点和最后一个点来确定
let path = getElementPathLinePoints(
Object.assign(this.startPoint, {w: 0, h: 0}),
Object.assign(this.endPoint, {w: 0, h: 0}));
let sp = path[0];
let ep = path[path.length - 1];
let secToLast = path.length > 2 ? path[path.length - 2] : sp
let pathPoints = path.slice(1, path.length - 1);
var arrowPoints = getArrowControlPoint(secToLast, ep)
ctx.moveTo(sp.x, sp.y);
pathPoints.forEach((item) => {
ctx.lineTo(item.x, item.y);
});
ctx.lineTo(ep.x, ep.y);
ctx.moveTo(arrowPoints.m.x, arrowPoints.m.y);
ctx.lineTo(ep.x, ep.y);
ctx.moveTo(arrowPoints.n.x, arrowPoints.n.y);
ctx.lineTo(ep.x, ep.y);
ctx.stroke();
ctx.closePath();
// 增加了一个逻辑,只有选段选中之后才出现拖拽点
if(this.active) {
this.children.forEach(item => {
item.draw(ctx)
})
}
}
}
这样就有一个只要传入起始点就能绘制一条带箭头的线段了,接下来就要完成吸附的操作了。
元素吸附
所谓元素吸附,就是拖拽线段的端点时,放下的时候落在哪个元素上,那么线段的端点就始终为该元素,由于文本节点是个矩形,我这里加了一个判断,落点除了在矩形内,更靠近哪条边就吸附在那条边的中点上,通过Stage提供的自定义事件,我们添加以下逻辑
class Connect {
constructor() {
// 省略
// 起点的点击事件,只要点了就开始把绑定元素清空
this.startPoint.addEvent("click", (t) => {
this.startBindTarget = null
})
this.startPoint.addEvent("move", (t) => {
// 在拖拽点的时候,把父元素线的active设为true,这样点才会一直显示
this.active = true
});
this.startPoint.addEvent("mouseup", (t) => {
// 拖拽结束后,判断落点是否有元素,如果有,以元素的xy为准,
// 同时记录元素,元素在更新位置的时候,也会更新线段
// 根据xy寻找到画布上的元素
let target = this.getMouseUpTarget(t.x, t.y)
// 更新 container已经虚化了,落点必定是container里的元素,所以绑定点为落点元素的parent
if(target.parent) {
// 箭头落点坐标计算方式要优化,不能直接用元素的顶点,要判断边,更接近哪条边就用哪条边
this.startBindTarget = target.parent[pointNearEdge(t, target)]
}
})
// endPoint同样处理
}
// draw()
}
这样在线段端点的事件操作时,我们可以完成连线的元素绑定,元素更新后,连线也能拿到新的xy从而绘制新的连线。
节点信息绑定
有了文本节点和连线节点,我们就可以完成一个简易的流程了,接下来只要在每个文本节点上添加一个点击事件,就能在上面绑定自定义的信息了。