canvas tools

本文介绍了一个名为CanvasDraw的类,它提供了一系列方法用于在HTML5canvas上绘制各种形状,如点、线、弧、椭圆、箭头和自定义路径。它还支持点样式设置和绘制状态管理,包括开始、结束、撤回和清空画布操作。
摘要由CSDN通过智能技术生成

canvas tools

canvas 常用工具,可以使用封装的工具实现在 canvas 上绘制点、直线、折现、圆、椭圆等图案

<canvas></canvas>
canvas {
    width: 1000px;
    height: 800px;
}
interface Pos {
  x: number;
  y: number;
}
interface StrokeStyle {
  width?: number;
  color?: string;
  fillColor?: string;
}
interface CanvasStrokeStyle {
  width: number;
  color: string;
  fillColor: string;
}

interface PointOptions {
  src?: string;
  width?: number;
  height?: number;
}

/**
 * 在 canvas 上绘制图形
 * @params canvas canvas dom
 * @params strokeStyle 可选 样式设置
 */
export default class CanvasDraw {
  canvas!: HTMLCanvasElement; // canvas 画布
  ctx!: CanvasRenderingContext2D; // canvas 上下文
  startPos!: Pos; // 开始位置
  imgDataList!: ImageData[]; // 画布像素数据列表
  method!: string | null; // 绘制方法
  moreMethod!: string | null; // 绘制类型
  strokeStyle!: CanvasStrokeStyle; // 样式
  pointList!: Pos[]; // 点坐标列表
  isDraw!: boolean; // more method 是否在绘制中
  drawList!: Pos[]; // type 为 draw 时 点坐标列表
  events!: object; // 发布订阅事件
  timer!: any; // 延时器
  pointStyle!: PointOptions; // 标注点的样式
  constructor(canvas: HTMLCanvasElement, strokeStyle?: StrokeStyle) {
    // 获取 canvas
    this.canvas = canvas;
    // 设置 canvas 宽高
    const { width, height } = canvas.getBoundingClientRect();
    this.canvas.width = width;
    this.canvas.height = height;
    // 获取 canvas 上下文
    const ctx: CanvasRenderingContext2D | null = canvas.getContext("2d", {
      willReadFrequently: true
    });
    if (!ctx) {
      new Error("canvas 上下文为 null");
      return;
    }
    this.ctx = ctx;
    // 画布像素数据列表默认为空
    this.imgDataList = [];
    // more method 的点坐标位置
    this.pointList = [];
    // more method 的绘制状态
    this.isDraw = false;
    // type 为 draw 时 点坐标列表
    this.drawList = [];
    // 设置默认样式
    const defineStrokeStyle = {
      width: 1,
      color: "#f00",
      fillColor: "rgba(255, 0, 0, 0.1)"
    };
    this.strokeStyle = defineStrokeStyle;
    for (const i in defineStrokeStyle) {
      if (strokeStyle?.["i"]) {
        this.strokeStyle[i] = strokeStyle["i"];
      }
    }
    this.events = {};
    this.drawEvent();
    // 监听屏幕大小变化
    window.addEventListener("resize", this.debounce());
  }
  // 获取样式
  get defineStrokeStyle() {
    return this.strokeStyle;
  }
  // 设置样式
  set defineStrokeStyle(style: StrokeStyle) {
    for (const i in style) {
      this.strokeStyle[i] = style[i];
    }
  }
  // 每次绘制时
  private init() {
    // 移除所有绘制的图像
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // 将之前绘制的图像贴到画布上
    this.imgDataList.length &&
      this.ctx.putImageData(this.imgDataList.slice(-1)[0], 0, 0);
  }
  /**
   * 开始绘制图像
   * @param type 绘制类型 暂时只支持 rect | arc | ellipse | arrow | draw 五种基本类型
   * 和 point | polyline | polygon 三种 more 类型
   * @param options 绘制的点样式,只有类型为 point 时需要传参
   */
  startDraw(type: string, pointStyle?: PointOptions) {
    this.endMoreDraw();
    const methodIndex = {
      rect: "drawRect",
      arc: "drawArc",
      ellipse: "drawEllipse",
      arrow: "drawArrow",
      draw: "drawDraw"
    };
    const method = methodIndex[type];
    if (method && method !== this.method) {
      this.method = method;
      this.moreMethod = null;
      return;
    }
    const moreMethodIndex = {
      point: "drawPoint",
      polyline: "drawPolyline",
      polygon: "drawPolygon"
    };
    const moreMethod = moreMethodIndex[type];
    if (type === "point") {
      // 如果是戳点获取样式
      this.pointStyle = pointStyle ?? {};
    }
    if (moreMethod && moreMethod !== this.moreMethod) {
      this.moreMethod = moreMethod;
      this.method = null;
    }
  }
  // 结束绘制
  endDraw() {
    if (this.method) {
      this.method = null;
    }
    if (this.moreMethod) {
      this.endMoreDraw();
      this.moreMethod = null;
    }
  }
  // 撤回画布
  back() {
    if (this.isDraw) {
      this.endMoreDraw();
      return;
    }
    const length = this.imgDataList.length; // 画布记录的数据列表
    if (!length) return;
    if (length === 1) {
      // 如果只有一个数据,清空画布
      this.clear();
      return;
    }
    if (length) {
      // 如果有多个,移除最后一个记录
      this.imgDataList.splice(length - 1, length);
      this.ctx.putImageData(this.imgDataList.slice(-1)[0], 0, 0);
    }
  }
  // 清空画布
  clear() {
    this.endMoreDraw();
    // 清空画布
    if (!this.imgDataList.length) return;
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // 清空记录的绘制记录列表
    this.imgDataList = [];
  }
  // 退出当前页面时移除监听的事件
  destroyEvent() {
    this.method = null;
    this.moreMethod = null;
    this.clear();
    this.pointList = [];
    this.drawList = [];
    this.timer && clearTimeout(this.timer);
    // 移除鼠标按下事件
    this.canvas.removeEventListener("mousedown", this.downEvent);
    // 移除鼠标移动事件
    this.canvas.removeEventListener("mousemove", this.moveEvent);
    // 移除鼠标弹起事件
    this.canvas.removeEventListener("mouseup", this.upEvent);
    // 移除鼠标点击事件
    this.canvas.removeEventListener("click", this.clickEvent);
    // 移除鼠标双击事件
    this.canvas.removeEventListener("dblclick", this.dblclickEvent);
    // 移除鼠标移动事件
    this.canvas.removeEventListener("mousemove", this.moveEvent);
    // 移除鼠标右键事件
    this.canvas.removeEventListener("contextmenu ", this.contextmenuEvent);
    // 移除屏幕大小监听事件
    window.removeEventListener("resize", this.debounce());
  }
  // 防抖
  debounce = () => {
    return () => {
      this.timer && clearTimeout(this.timer);
      this.timer = setTimeout(() => {
        this.resize();
      }, 300);
    };
  };
  // 修改画布大小
  private resize = () => {
    // 设置 canvas 宽高
    const { width, height } = this.canvas.getBoundingClientRect();
    this.canvas.width = width;
    this.canvas.height = height;
    // TODO: 如果不清除画布,之前绘制的图形会错位
    this.clear();
  };
  // 订阅事件
  on(name: string, callback: any) {
    if (!this.events[name]) {
      this.events[name] = [];
    }
    this.events[name].push(callback);
  }
  // 发布事件
  private emit(name: string, ...args: any[]) {
    if (this.events[name]) {
      this.events[name].forEach((callback) => {
        callback(...args);
      });
    }
  }
  // 取消订阅
  off(name: string, callback: any) {
    if (this.events[name]) {
      this.events[name] = this.events[name].filter(
        (item: any) => item !== callback
      );
    }
  }
  // 结束 more method 的绘制中的绘制
  private endMoreDraw() {
    if (this.isDraw) {
      // 如果处于绘制状态
      // 清空记录的点坐标列表
      this.pointList = [];
      // 结束绘制状态
      this.finishMoreDraw();
    }
  }
  // 结束 more method 的绘制
  private finishMoreDraw() {
    // 需要满足的最少点
    const pointLength = {
      drawPolyline: 2,
      drawPolygon: 3
    };
    this.init();
    const length = this.pointList.length;
    if (this.moreMethod && pointLength[this.moreMethod] <= length) {
      // 如果有点坐标信息且满足最少点,绘制并保存
      this[this.moreMethod]();
      this.emit("drawEnd", this.pointList);
      // 保存当前画布上的图像
      this.imgDataList.push(
        this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
      );
    }
    // 清空坐标信息
    this.pointList = [];
    // 修改绘制状态
    this.isDraw = false;
    // 移除鼠标双击事件
    this.canvas.removeEventListener("dblclick", this.dblclickEvent);
    // 移除鼠标右键事件
    this.canvas.removeEventListener("contextmenu ", this.contextmenuEvent);
    // 移除鼠标移动事件
    this.canvas.removeEventListener("mousemove", this.moveEvent);
  }
  // 双击事件
  private dblclickEvent = () => {
    const length = this.pointList.length;
    // 移除双击产生的最后多余的点
    this.pointList.splice(length - 1, length);
    this.finishMoreDraw();
  };
  // 右击事件
  private contextmenuEvent = (e: any) => {
    // 阻止默认事件
    e.preventDefault();
    this.finishMoreDraw();
  };
  // 点击事件
  private clickEvent = (e: any) => {
    if (!this.moreMethod) return;
    const pos = {
      x: e.offsetX,
      y: e.offsetY
    };
    if (this.moreMethod === "drawPoint") {
      // 如果是戳点
      this[this.moreMethod](e);
      // 不需要用到移动等事件
      return;
    }
    // 修改绘制状态
    this.isDraw = true;
    // 记录当前戳的点坐标
    this.pointList.push(pos);
    // 监听鼠标双击事件
    this.canvas.addEventListener("dblclick", this.dblclickEvent);
    // 监听鼠标右键事件
    this.canvas.addEventListener("contextmenu", this.contextmenuEvent);
    // 监听鼠标移动事件
    this.canvas.addEventListener("mousemove", this.moveEvent);
  };
  // 移动事件
  private moveEvent = (e: any) => {
    this.init();
    if (this.method) {
      this[this.method](e);
      return;
    }
    if (this.moreMethod) {
      this[this.moreMethod](e);
    }
  };
  // 鼠标弹起事件
  private upEvent = (e: any) => {
    // 保存当前画布上的图像
    this.imgDataList.push(
      this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
    );
    if (this.drawList.length) {
      // 如果是 type 为 draw 时结束
      this.drawList = [];
    }
    // 移除鼠标移动事件
    this.canvas.removeEventListener("mousemove", this.moveEvent);
    // 移除鼠标弹起事件
    this.canvas.removeEventListener("mouseup", this.upEvent);
    const res = {
      startPos: this.startPos,
      endPos: {
        x: e.offsetX,
        y: e.offsetY
      }
    };
    this.emit("drawEnd", res);
  };
  // 鼠标按下事件
  private downEvent = (e: any) => {
    // 没有选中类型不绘制
    if (!this.method) return;
    this.startPos = {
      x: e.offsetX,
      y: e.offsetY
    };
    if (this.method === "drawDraw") {
      // 如果 type 为 draw 时
      this.drawList.push(this.startPos);
    }
    // 监听鼠标移动事件
    this.canvas.addEventListener("mousemove", this.moveEvent);
    // 监听鼠标弹起事件
    this.canvas.addEventListener("mouseup", this.upEvent);
  };
  // 鼠标事件
  private drawEvent() {
    // 监听鼠标按下事件
    this.canvas.addEventListener("mousedown", this.downEvent);
    // 监听鼠标点击事件
    this.canvas.addEventListener("click", this.clickEvent);
  }
  // 绘制矩形
  private drawRect(e: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    // 绘制
    const { x, y } = this.startPos;
    this.ctx.rect(x, y, e.offsetX - x, e.offsetY - y);
    this.ctx.stroke();
  }
  // 绘制圆
  private drawArc(e: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    // 绘制
    const { x, y } = this.startPos;
    const centerX = Math.abs(e.offsetX + x) / 2;
    const centerY = Math.abs(e.offsetY + y) / 2;
    const radius = Math.abs(e.offsetX - x) / 2;
    this.ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, true);
    this.ctx.stroke();
  }
  // 绘制椭圆
  private drawEllipse(e: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    // 绘制
    const { x, y } = this.startPos;
    const centerX = Math.abs(e.offsetX + x) / 2;
    const centerY = Math.abs(e.offsetY + y) / 2;
    const radiusX = Math.abs(e.offsetX - x) / 2;
    const radiusY = Math.abs(e.offsetY - y) / 2;
    this.ctx.ellipse(
      centerX,
      centerY,
      radiusX,
      radiusY,
      0,
      0,
      2 * Math.PI,
      true
    );
    this.ctx.stroke();
  }
  // 绘制箭头
  private drawArrow(e: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    // 绘制
    // 线段部分
    const { x, y } = this.startPos;
    this.ctx.moveTo(x, y);
    this.ctx.lineTo(e.offsetX, e.offsetY);
    // 箭头部分
    const arrowDeg = 20; // 箭头夹角
    const arrowLength = Math.min(10 * width, 50); // 箭头长度
    // 上半部分箭头
    const radius1 =
      arrowDeg * (Math.PI / 180) + Math.atan2(e.offsetY - y, e.offsetX - x);
    const arrow1 = { x: 0, y: 0 }; // 箭头坐标
    arrow1.x = e.offsetX - Math.cos(radius1) * arrowLength;
    arrow1.y = e.offsetY - Math.sin(radius1) * arrowLength;
    this.ctx.moveTo(e.offsetX, e.offsetY);
    this.ctx.lineTo(arrow1.x, arrow1.y);
    // 下半半部分箭头
    const radius2 =
      arrowDeg * (Math.PI / 180) - Math.atan2(e.offsetY - y, e.offsetX - x);
    const arrow2 = { x: 0, y: 0 }; // 箭头坐标
    arrow2.x = e.offsetX - Math.cos(radius2) * arrowLength;
    arrow2.y = e.offsetY + Math.sin(radius2) * arrowLength;
    this.ctx.moveTo(e.offsetX, e.offsetY);
    this.ctx.lineTo(arrow2.x, arrow2.y);
    this.ctx.stroke();
  }
  // 绘画
  private drawDraw(e: any) {
    const pos = { x: e.offsetX, y: e.offsetY };
    this.drawList.push(pos);
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    // 绘制
    for (let i = 0; i < this.drawList.length; i++) {
      const { x, y } = this.drawList[i];
      if (i === 0) {
        // 第一个点
        this.ctx.moveTo(x, y);
      } else {
        // 其他的点
        this.ctx.lineTo(x, y);
      }
    }
    this.ctx.stroke();
  }
  // 绘制点
  private drawPoint(e: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { offsetX, offsetY } = e;
    const { color } = this.strokeStyle;
    this.ctx.fillStyle = color;
    const { src, width, height } = this.pointStyle;
    const imageWidth = width ?? 20;
    const imageHeight = height ?? 20;
    // 绘制
    if (src) {
      // 有图片地址,绘制图片
      const image = new Image();
      image.src = src;
      image.onload = () => {
        this.ctx.drawImage(
          image,
          offsetX - imageWidth / 2,
          offsetY - imageHeight / 2,
          imageWidth,
          imageHeight
        );
        // 保存当前画布上的图像
        this.imgDataList.push(
          this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
        );
      };
    } else {
      // 无图片地址,绘制圆点
      this.ctx.ellipse(
        offsetX,
        offsetY,
        imageWidth / 2,
        imageHeight / 2,
        0,
        0,
        2 * Math.PI,
        true
      );
      this.ctx.fill();
      // 保存当前画布上的图像
      this.imgDataList.push(
        this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
      );
    }
  }
  // 绘制折线
  private drawPolyline(e?: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    // 绘制
    for (let i = 0; i < this.pointList.length; i++) {
      const { x, y } = this.pointList[i];
      if (i === 0) {
        // 第一个点
        this.ctx.moveTo(x, y);
      } else {
        // 其他的点
        this.ctx.lineTo(x, y);
      }
    }
    // 添加移动的点
    e && this.ctx.lineTo(e.offsetX, e.offsetY);
    this.ctx.stroke();
  }
  // 绘制多边形
  private drawPolygon(e?: any) {
    // 开始绘制
    this.ctx.beginPath();
    // 设置绘制样式
    const { width, color, fillColor } = this.strokeStyle;
    this.ctx.lineWidth = width;
    this.ctx.strokeStyle = color;
    this.ctx.fillStyle = fillColor;
    // 绘制
    for (let i = 0; i < this.pointList.length; i++) {
      const { x, y } = this.pointList[i];
      if (i === 0) {
        // 第一个点
        this.ctx.moveTo(x, y);
      } else {
        // 其他的点
        this.ctx.lineTo(x, y);
      }
    }
    // 添加移动的点
    e && this.ctx.lineTo(e.offsetX, e.offsetY);
    // 连接第一个点
    const { x, y } = this.pointList[0];
    this.ctx.lineTo(x, y);
    this.ctx.stroke();
    this.ctx.fill();
  }
}

demo

const canvasDraw = new CanvasDraw(canvas); // 实例化
const type = "rect" // "rect" | "arc" | "ellipse" | "arrow" | "draw" | "polyline" | "polygon" 类型
canvasDraw.setStrokeStyle = { width: 1, color: "red" }; // 设置样式
canvasDraw.startDraw(type); // 开始绘制 选择类型
canvasDraw.endDraw(); // 结束绘制 取消选择类型
canvasDraw.back(); // 撤回画布
canvasDraw.clear(); // 清空画布
canvasDraw.destroyEvent(); // 退出页面时的操作
自定义您的Canvas体验,并使用您的Canvas by Instructure帐户进行高级操作。 功能•自定义颜色•从任何选项卡向学生发送消息•有人向您发送消息时会收到通知•离线下载Canvas模块•只需右键单击即可将文件作为作业提交•使用编辑器将自定义元素添加到您的内容中•搜索页面,使用Chrome多功能工具的课程,组和作业•在会话中添加“全部标记为已读”按钮•自动登录•与Canvas API集成•使用编辑器将自定义按钮,徽章等添加到帖子中•轻松设置页脚编辑器,然后通过右键单击Canvas Canvas管理员来复制并粘贴它,请访问我们的网站,以了解更多有关我们如何免费为您的机构提供帮助的信息! CanvasTools是一组高级工具,使您可以自定义Canvas by Instructure的外观和使用Canvas帐户进行高级操作的方式。即使您的机构已经设置了画布,也要更改其默认颜色。您使用CanvasTools进行的美学更改仅出现在您的CanvasCanvasTools可以与使用Instructure子域的任何网站一起使用。借助CanvasTools,您可以从任何选项卡向同行发送消息。无需打开Canvas即可向朋友发送消息 CanvasTools不断收到具有新功能的新更新。我们希望改善您在Canvas上的生活!更新日志:https://github.com/jczstudios/canvastools/wiki/Update-Log * CanvasTools不隶属于Instructure。请不要与Canvas联系以获取有关此扩展程序的帮助,请改用支持工具 ** CanvasTools的创建者不容忍将这些工具用于任何作弊,欺骗,垃圾邮件等行为,使用后果自负。 支持语言:English (United States)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值