mxGraph zoom性能优化

555 阅读3分钟

简介

这个文章主要讨论在mxGraph进行放大缩小的时候遇到的性能和中心点计算问题。

Pinch 性能问题

MxGraph 在mxPanningHandler 中实现的Pinch功能直接调用的zoom

// mxPanningHandler 文件
this.gestureHandler = mxUtils.bind(this, function(sender, eo)
        {
            if (this.isPinchEnabled())
            {
                var evt = eo.getProperty('event');
                
                if (!mxEvent.isConsumed(evt) && evt.type == 'gesturestart')
                {
                    this.initialScale = this.graph.view.scale;
                
                    // Forces start of panning when pinch gesture starts
                    if (!this.active && this.mouseDownEvent != null)
                    {
                        this.start(this.mouseDownEvent);
                        this.mouseDownEvent = null;
                    }
                }
                else if (evt.type == 'gestureend' && this.initialScale != null)
                {
                    this.initialScale = null;
                }
                
                if (this.initialScale != null)
                {
                    this.zoomGraph(evt);
                }
            }
        });

每次手指移动缩放zoomGraph 都会遍历每一个node进行计算位置和绘制,几十个结点的时候,缩放操作就会很卡顿,图形越大越卡顿。

drawio 使用的是官方缩放算法,结点过多的时候掉帧严重(180ms一帧),操作卡顿。

drawIo在缩放上面写了上百行代码对mxGraph进行优化,lazyZoom缩放操作scheduleZoom 方法进行调度缩放任务但是效果一般。

image.png

实现自定义Pinch算法

  1. 当缩放的时候

  2. 计算当前scale和鼠标为中心的偏移值 3. Canvas 元素设置样式 transform: scale translate // 注意scale 和translate的前后顺序影响很大,transfrom的执行顺序是从右到左

  3. 当缩放结束

  4. 移除画布上的transform: scale

transform: scale translate 注意scale 和translate的前后顺序影响很大,transfrom的执行顺序是从右到左

自定义算法性能

可以看到各种缩放操作下性能面板程序绿色。没有出现drawio那样的大范围掉帧。

不过性能中依然有一定频率的掉帧。原因就是缩放结束的时候,同步给mxGraph进行绘制的时候,mxGraph遍历了每个结点进行位置计算和重新绘制

Pinch 中心点计算

每次缩放,都要围绕着鼠标当前位置进行缩放,需要重新计算画布中结点的位置和scale。MxGraph不支持此操作。

Issue 中的方案使用官方缩放算法,性能极差。

mxEvent.addMouseWheelListener((evt, up) => {
  if (mxEvent.isConsumed(evt)) {
    return;
  }

  let gridEnabled = graph.gridEnabled;

  // disable snapping
  graph.gridEnabled = false;

  let p1 = graph.getPointForEvent(evt, false);

  if (up) {
    graph.zoomIn(); // 
  } else {
    graph.zoomOut();
  }

  let p2 = graph.getPointForEvent(evt, false); // 通过缩放前后的差异来获取偏移值,性能特别差
  let deltaX = p2.x - p1.x;
  let deltaY = p2.y - p1.y;
  let view = graph.view;

  view.setTranslate(view.translate.x + deltaX, view.translate.y + deltaY); // 每次缩放都进行大量结点计算

  graph.gridEnabled = gridEnabled;

  mxEvent.consume(evt);
}, container);

whiteboard_exported_image.png

        let {
            origin: [ox, oy],
            first,
            movement: [ms],
            memo,
            offset: [offsetScale]
          } = state
          const { translateX, translateY, scale: pScale } = getTransform(canvas)
         if (first) {
            const { x: containerX, y: containerY } =
              container.getBoundingClientRect()
            const tx = ox - containerX // 缩放中心点ox 减去画布左边x获得画布坐标系中中心点x值
            const ty = oy - containerY
            memo = [tx, ty]
          }
          if (state.delta[0] > 0) {
            ms = 0.1 * SCALE_SPEED
          } else {
            ms = -1 * 0.1 * SCALE_SPEED
          }
          const scale = offsetScale
          const factor = scale / pScale // 本次缩放比率

          let x = ((1 - factor) * memo[0]) / scale + translateX // 缩放中心点计算核心算法
          let y = ((1 - factor) * memo[1]) / scale + translateY // 由于mxGraph计算的位置的时候先加translate然后乘以scale,所以这里必须处以scale

          canvas.style.transform = `scale(${scale},${scale}) translate(${x}px,${+y}px) `

          store.commit('workflowStore/updateCanvasScale', scale * view.scale)
          return memo

边界计算借用use-gesture 的bounds 来实现。计算bounds要明确知道边界是根据state上面的offset来进行判定的。offset是一个手势整个生命周期的偏移量。

我们在一次手势例如pinch和pinchend的时候要对offset进行重置,这样边界计算才是正确的。