• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

踩坑小程序canvas,撤销操作实现

武飞扬头像
末日码农
帮助1

小程序搜索【涂图了】,可进行体验。

学新通

轨迹自动播放实现

在画布上绘制笔迹的时候,通过touchmove事件将每一次绘制的点坐标都记录起来,同时也要将当前笔迹的颜色、宽度等数据记录起来。最终画布上所有的笔迹都转换成了一个数据列表,当播放的时候,再将列表里面的数据一条一条的在canvas上重新绘制出来,通过setTimeout进行自动循环不断的去绘制。

看代码:

class Paint {
  // ...省略...
  /**
   * 从头绘制路径
   * @param path 路径
   * @param completed 完成回调
   * @returns
   */
  playPath(path: Path[], completed?: () => void) {
    if (!path.length) return Promise.resolve();

    this.path = path;

    // 初始化
    this.row = 0;
    this.column = 0;
    this.stop = false;
    this.isComplete = false;

    const { pos, color, width } = path[0];
    this.start(pos[0], color, width);

    this.run(completed);
    if (completed) {
      this.completed = completed;
    }
  }

  private completed() {}

  private run(completed = this.completed) {
    // 结束绘制(下一次播放的时候要结束上一次播放)
    if (this.stop) {
      return;
    }

    if (this.column < this.path[this.row].pos.length) {
      // 绘制第 n 条轨迹
      this.drawLine(this.path[this.row].pos[this.column  ]);
      setTimeout(() => this.run(), 16.7);
    } else {
      // 一条轨迹制完成
      if (  this.row < this.path.length) {
        // 初始化下一条轨迹
        this.column = 0;
        const { pos, color, width } = this.path[this.row];
        this.start(pos[0], color, width);

        // 延时一会儿开始绘制下一条轨迹
        setTimeout(() => {
          setTimeout(() => this.run(), 16.7);
        }, 240);
      } else {
        // 结束
        this.isComplete = true;
        completed();
      }
    }
  }

  /**
   * 暂停播放
   */
  pause() {
    this.stop = true;
  }

  /**
   * 继续播放
   * @param completed 完成回调
   */
  play(completed?: () => void) {
    this.stop = false;
    this.run(completed);
  }
}
学新通

有些命名可能有点不规范,逻辑也有一点复杂。代码中path数据列表保存的是每一次绘制的笔迹,每一条笔迹又包含了一组坐标点、颜色和宽度,类型定义如下:

export interface Dot {
  x: number;
  y: number;
}

export interface Path {
  pos: Dot[];
  color?: string;
  width?: number;
}

最终绘制的时候都是在处理点坐标,就相当于是在处理一个二维数组,所有就通过rowcolumn变量来进行区分。

撤销操作

主要思路就是:每在画布上进行一次绘制,在结束的时候touchend就将当前画布上的数据保存起来,通过ctx.getImageDataapi可以保存当前画布,然后放在一个列表中。同时再用一个变量作为指针来记录当前所在列表的位置,没点击一次撤销操作,指针就在列表中往前移动一步,同理点击恢复撤销时,指针往后移动一步。需要注意的一个点就是,当进行n次撤销操作后,指针不在列表最后的位置时,再进行绘制,此时应该清空列表中指针所在位置后面的数据,并保存当前画布数据。因为每次在画布上的操作,都应该是列表中的最后一条数据。

const handleTouchEnd = () => {
    if (!painting) return;
    painting = false;

    // 步骤  1
    commit(TypeKeys.OPERATION_ADD);

    // 保存路径
    const path = state.path.slice(0, state.currentPathIndex);
    commit(TypeKeys.SET_PATH, path.concat(currentLine));

    // 生成记录
    const list = state.historyStepList.slice(0, state.currentStepIndex >= MAX_HISTORY_COUNT - 1 ? state.currentStepIndex   1 : state.currentStepIndex);
    commit(TypeKeys.SET_HISTORY_STEP_LIST, list.concat(paint.value?.getImageData()));
};

这里主要的难点就是对指针的控制,尤其是在列表的临界点时的处理,因为我这里还需要控制路径的列表,所有更加复杂一些,这一块差不多花了我一天的时间,处理不好在撤销的时候就很容易出bug。

由于每次保存的画布数据是比较大的,必须对历史记录列表做一个最大长度的限制,否则就会撑爆内存,小程序闪退。getImageData获取的画布数据大小取决于画布的width*height的大小,由于我这个小程序是全屏的,所以在大屏幕上的数据会非常的大,我目前限制了最大历史记录为10,后期考虑固定画布的大小。

不同大小的画布还会存在一个问题,就是在小屏幕手机上,可能会展示不全大屏幕手机分享的内容。

未完待续

最后,再放出小程序码,欢迎大家进行体验。

学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhieghaj
系列文章
更多 icon
同类精品
更多 icon
继续加载