前段时间做了图形在线绘制功能,使用了Konvajs,以下简单举例konvajs一小部分应用场景。
官网地址:konvajs-doc.bluehymn.com/
创建图形
konvajs封装了canvas api,在创建图形时,将复杂琐碎api封装为大概5个步骤,套用相同的模式,在创建图形对象时候换为创建不同图形对象就可以了,用起来比较简单。
绘制图形-矩形
鼠标绘制矩形使用new Konva.Rect,其中属性x,y为起始坐标点位置,width、height分别为图形宽高信息,因此鼠标落下时记录起始坐标位置为矩形起始位置。鼠标移动时实时获取当前鼠标位置为矩形终止位置,由此实时更新width、height宽高信息。注意别忘记鼠标抬起时清空图形对象,否则不会停止绘制。
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
let rect;
let startPos;
stage.on('mousedown', () => {
const pos = stage.getPointerPosition();
startPos = pos;
rect = new Konva.Rect({
x: pos.x,
y: pos.y,
fill: 'red', // 填充颜色
name: 'rect',
stroke: 'black', // 边框颜色
draggable: true // 允许拖动
});
layer.add(rect);
});
stage.on('mousemove', () => {
if (rect) {
const pos = stage.getPointerPosition();
// 此处可以添加优化,移动长度小于5像素不绘制,避免误触(勾股定理终于没白学)
if(Math.sqrt((startPos.x-pos.x) ** 2+(startPos.y-pos.y) **2 )>5){
rect.setAttrs({
width:Math.abs(startPos.x-pos.x),
height:Math.abs(startPos.y-pos.y)
});
layer.batchDraw();
}
}
});
stage.on('mouseup', () => {
rect = null;
});
layer.draw();
绘制图形-圆形
鼠标绘制圆形和绘制矩形一模一样,在创建对象时候使用 new Konva.Circle,标识圆形大小的属性是radius,所以通过记录鼠标点下时坐标点和移动时实时坐标位置,通过勾股定理计算直径大小。
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
let circle;
let startPos;
stage.on('mousedown', () => {
const pos = stage.getPointerPosition();
startPos = pos;
circle = new Konva.Circle({
x: pos.x,
y: pos.y,
fill: 'red',
name: 'circle',
stroke: 'black',
draggable: true
});
layer.add(circle);
});
stage.on('mousemove', () => {
if (circle) {
const pos = stage.getPointerPosition();
// 此处可以添加优化,移动长度小于5像素不绘制,避免误触
if(Math.sqrt((startPos.x-pos.x) ** 2+(startPos.y-pos.y) **2 )>5){
circle.setAttrs({
radius:Math.sqrt((startPos.x-pos.x) ** 2+(startPos.y-pos.y) **2 )
});
layer.batchDraw();
}
}
});
stage.on('mouseup', () => {
circle = null;
});
layer.draw();
绘制箭头
普通箭头
鼠标绘制箭头和以上相同,区别在于points标识箭头位置,points数组中前两个元素标识起始坐标x,y轴位置,鼠标移动过程中更新后两个元素为当前坐标点x,y位置。
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const shape = new Konva.Text({
text: 'Try to draw an arrow'
});
layer.add(shape);
let arrow;
stage.on('mousedown', () => {
const pos = stage.getPointerPosition();
arrow = new Konva.Arrow({
points: [pos.x, pos.y],
stroke: 'black',
fill: 'black'
});
layer.add(arrow);
layer.batchDraw();
});
stage.on('mousemove', () => {
if (arrow) {
const pos = stage.getPointerPosition();
const points = [arrow.points()[0], arrow.points()[1], pos.x, pos.y];
arrow.points(points);
layer.batchDraw();
}
});
stage.on('mouseup', () => {
arrow = null;
});
layer.draw();
箭头连线
鼠标绘制箭头连线(现在设定的是每个小箭头长度为25像素),我做的时候一直想不好应该怎么使箭头方向朝着鼠标移动方向,和朋友交流过之后想到,鼠标落下时记录起止位置(x1,y1),移动过程中记录终止位置(x2,y2),中间不断去更新偏移位置为25像素就绘制一个箭头,应用等边三角形同边比例相等计算每个小箭头终止位置。图画的比较丑:
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
// 画箭头
const layer = new Konva.Layer();
stage.add(layer);
let arrow;
stage.on('mousedown', () => {
const pos = stage.getPointerPosition();
arrow = new Konva.Arrow({
points: [pos.x, pos.y],
stroke: 'black',
fill: 'black'
});
layer.add(arrow);
layer.batchDraw();
});
stage.on('mousemove', () => {
if (arrow) {
console.log(arrow.points()[0])
const pos = stage.getPointerPosition();
const x1 = arrow.points()[0];
const y1 = arrow.points()[1];
const x2 = pos.x;
const y2 = pos.y;
const a =x2-x1;//坐标在x轴方向的位移量
const b =y2-y1;//坐标在y轴方向的位移量
const c = Math.sqrt(a ** 2 + b ** 2);//鼠标本次两端间位置量
//x3为小箭头坐标x轴方向位置,斜边固定为25像素,那x轴方向位置量(x3-x1)/a=25/c
const x3 = 25/c*a+x1;
//与以上同理
const y3 = 25/c*b+y1;
console.log('x3',x3,';y3',y3)
// 两点间位移量大于25像素,就绘制一个箭头
if(c>25){
const pos = stage.getPointerPosition();
arrow = new Konva.Arrow({
points: [x2, y2],
stroke: 'black',
fill: 'black'
});
layer.add(arrow);
layer.batchDraw();
}else{
const points = [x1, y1, x3, y3];
arrow.points(points);
}
layer.batchDraw();
}
});
stage.on('mouseup', () => {
arrow = null;
});
layer.draw();
直线箭头
鼠标绘制直线连线的箭头时候,刚开始思路一直没打开,考虑怎么能够实时获取鼠标方向并更新所有小箭头位置。跟朋友沟通过后,其实不必计较绘制过程中的问题,仅仅记录鼠标落下时候位置为起点,抬起时候为终点,从中截取为n个小箭头,绘制过程中使用一个有透明度的大箭头。这样并不会影响用户使用,计算就简单太多了。
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const shape = new Konva.Text({
text: 'Try to draw an arrow'
});
layer.add(shape);
let arrow;
let startPos = null;
stage.on('mousedown', () => {
const pos = stage.getPointerPosition();
startPos = pos;
arrow = new Konva.Arrow({
points: [pos.x, pos.y],
stroke: 'rgba(76, 175, 80, 0.5)',
fill: 'rgba(76, 175, 80, 0.5)'
});
layer.add(arrow);
layer.batchDraw();
});
stage.on('mousemove', () => {
if (arrow) {
const pos = stage.getPointerPosition();
const points = [arrow.points()[0], arrow.points()[1], pos.x, pos.y];
arrow.points(points);
layer.batchDraw();
}
});
stage.on('mouseup', () => {
arrow.remove()
arrow = null;
const pos = stage.getPointerPosition();
const x = pos.x-startPos.x;
const y = startPos.y-pos.y;
const c = Math.sqrt(x**2+y**2);
const count = Math.round(c / 25);
const tempx=x/count;
const tempy=y/count;
let oldX=startPos.x;
let oldY = startPos.y;
// 其实这里把循环创建出来的箭头放到一个group中,然后所有的事件都绑定到group中是最好的,这里纯为演示,引导思路,就不改了
for(let i=1;i<count+1;i++){
const newarrow = new Konva.Arrow({
points: [oldX,oldY,startPos.x+tempx*i,startPos.y-tempy*i],
stroke: 'rgba(76, 175, 80)',
fill: 'rgba(76, 175, 80)'
});
layer.add(newarrow);
layer.batchDraw();
oldX=startPos.x+tempx*i+tempx/5;
oldY = startPos.y-tempy*i-tempy/5;
}
});
layer.draw();
拖拽位置
官网写的非常明白,只需要设置需要拖拽的对象draggable为true,起初以为需要自己监听鼠标移动,实时计算位移偏移量,其实konvajs都已经封装好了,简直太棒了!
官网地址:konvajs-doc.bluehymn.com/docs/drag_a…
我们可以设置 draggable 为 true 或者使用 draggable() 方法使图形可以被拖拽。draggable() 方法会自动适配桌面端和移动端。
我们通过 on() 方法监听节点的 dragstart、 dragmove、 dragend 等拖拽事件,on() 方法需要传入事件类型和事件发生时执行的函数。
图形变换缩放
图形变换调整尺寸及方向 官网这个案例就很好。
添加动画
添加动画是通过调用new Konva.Animation,相当于创建了一个定时器,传入的回调函数在定时器中执行。这个时候要注意在内部去修改某对象属性时,可能会造成不停的重绘重排,导致浏览器卡顿,这个时候可以通过回调函数中做判断,减少绘制次数来优化性能,如图中不符合条件的return false。
导出json
导出json就一句代码,实在是太好用了!
stage.toJSON();
总结
konvajs确实为我们省去canvas大量api的编写,总体步骤:
- 先创建stage舞台,创建layer图层添加到stage中。
- 创建图形对象,添加到对应的layer图层中。
- 监听图形对象触发事件或 舞台事件 键盘事件或通过id 选择器 type 选择器 name 选择器,对图形进行修改样式/拖拽/变形/创建动画等效果操作。
- 序列化舞台数据用于存储,或读取后渲染为图形。
其他:
-
在实际应用过程中,可能场景比较复杂,事件监听尽量绑定至stage上,方便统一处理。但是一定逻辑清晰,千万别给自己埋坑,不要让该监听到的事件直接拦截住,尽量将错误处理做好控制台打印,方便自己排查问题。(我遇到的一个问题,在同一个layer上绘制时,绘制过程中触碰到其他图形停止绘制问题,经查看代码是其他图形同样拦截了mouseover事件,内部处理释放了当前绘制对象;)
-
大图渲染也会导致浏览器卡顿,所以上传图片时一定要进行图片压缩,个人建议尽量不要超过1m(经验而谈,没有什么数据支持依据)。
-
创建layer对象时,最好在3-5个,每创建一个layer会创建一个canvas对象,当大于5个有可能会造成内存溢出。
-
动画如果绑定在了多个图形对象上,可能会造成卡顿,所以尽量优化动画的执行次数。
-
使用完毕动画对象一定要及时销毁,否则定时器会一直执行。