基于Konvajs(Canvas 库)的应用

2,005 阅读7分钟

前段时间做了图形在线绘制功能,使用了Konvajs,以下简单举例konvajs一小部分应用场景。

官网地址konvajs-doc.bluehymn.com/

创建图形

konvajs封装了canvas api,在创建图形时,将复杂琐碎api封装为大概5个步骤,套用相同的模式,在创建图形对象时候换为创建不同图形对象就可以了,用起来比较简单。

image.png

绘制图形-矩形

image.png

鼠标绘制矩形使用new Konva.Rect,其中属性x,y为起始坐标点位置,width、height分别为图形宽高信息,因此鼠标落下时记录起始坐标位置为矩形起始位置。鼠标移动时实时获取当前鼠标位置为矩形终止位置,由此实时更新width、height宽高信息。注意别忘记鼠标抬起时清空图形对象,否则不会停止绘制。

演示地址jsbin.com/xehamotoka/…

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();

绘制图形-圆形

image.png

演示地址:jsbin.com/nudamabena/…

鼠标绘制圆形和绘制矩形一模一样,在创建对象时候使用 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();

绘制箭头

普通箭头

image.png

演示地址:jsbin.com/bipajikuwe/…

鼠标绘制箭头和以上相同,区别在于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();

箭头连线

image.png

演示地址:jsbin.com/juxakobimu/…

鼠标绘制箭头连线(现在设定的是每个小箭头长度为25像素),我做的时候一直想不好应该怎么使箭头方向朝着鼠标移动方向,和朋友交流过之后想到,鼠标落下时记录起止位置(x1,y1),移动过程中记录终止位置(x2,y2),中间不断去更新偏移位置为25像素就绘制一个箭头,应用等边三角形同边比例相等计算每个小箭头终止位置。图画的比较丑:

1671288833434.png

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();

直线箭头

image.png

演示地址:jsbin.com/ruwogozuqu/…

鼠标绘制直线连线的箭头时候,刚开始思路一直没打开,考虑怎么能够实时获取鼠标方向并更新所有小箭头位置。跟朋友沟通过后,其实不必计较绘制过程中的问题,仅仅记录鼠标落下时候位置为起点,抬起时候为终点,从中截取为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() 方法需要传入事件类型和事件发生时执行的函数。

image.png

图形变换缩放

图形变换调整尺寸及方向 官网这个案例就很好。

image.png

添加动画

添加动画是通过调用new Konva.Animation,相当于创建了一个定时器,传入的回调函数在定时器中执行。这个时候要注意在内部去修改某对象属性时,可能会造成不停的重绘重排,导致浏览器卡顿,这个时候可以通过回调函数中做判断,减少绘制次数来优化性能,如图中不符合条件的return false

image.png

导出json

导出json就一句代码,实在是太好用了!

stage.toJSON();

总结

konvajs确实为我们省去canvas大量api的编写,总体步骤:

  1. 先创建stage舞台,创建layer图层添加到stage中。
  2. 创建图形对象,添加到对应的layer图层中。
  3. 监听图形对象触发事件舞台事件 键盘事件或通过id 选择器 type 选择器 name 选择器,对图形进行修改样式/拖拽/变形/创建动画等效果操作。
  4. 序列化舞台数据用于存储,或读取后渲染为图形。

其他:

  1. 在实际应用过程中,可能场景比较复杂,事件监听尽量绑定至stage上,方便统一处理。但是一定逻辑清晰,千万别给自己埋坑,不要让该监听到的事件直接拦截住,尽量将错误处理做好控制台打印,方便自己排查问题。(我遇到的一个问题,在同一个layer上绘制时,绘制过程中触碰到其他图形停止绘制问题,经查看代码是其他图形同样拦截了mouseover事件,内部处理释放了当前绘制对象;)

  2. 大图渲染也会导致浏览器卡顿,所以上传图片时一定要进行图片压缩,个人建议尽量不要超过1m(经验而谈,没有什么数据支持依据)。

  3. 创建layer对象时,最好在3-5个,每创建一个layer会创建一个canvas对象,当大于5个有可能会造成内存溢出。

  4. 动画如果绑定在了多个图形对象上,可能会造成卡顿,所以尽量优化动画的执行次数。

  5. 使用完毕动画对象一定要及时销毁,否则定时器会一直执行。