vue3+fabric绘制检测区域

概述

Vue3.2.x出来之后,调研了下周边相关的东西,觉着在新项目中可以使用,于是乎,先弄个Demo试试水。

技术:

vite2.x+vue3.2.6+fabric+ant-design-vue
当然fabric之前没使用过,所以也参考了网上一些其他人使用方式,非常感谢!!!

vite和vue3使用感受

使用vite开发,启动和打包确实快,是个不错的脚手架选择。

现在vue3 使用composition api写法,比起之前更加的碎片化,要记得的知识点也多了些,之前几乎就是this横扫一切,要是写过react的同学,一定不会陌生,因为这种写法和react相似,尤其是再加上jsx之后。
setup这个可以说直接隐去了之前的生命周期,当然,在需要的时候仍然可以调用,这个就有些和react的函数组件有些相似。
vue3可以说是集百家之长,来不断的提升自己,写法上多种多样,还是比较适合新手入门的。

GitHub地址:点我

效果图

vue3+fabric绘制效果

绘制核心代码

//新建文件img-maker.vue
<template>
  <div v-show="state.loading" class="loading-box">
    <LoadingOutlined style="font-size: 30px; color: #ea5413" />资源加载中...
  </div>
  <div v-show="!state.loading">
    <canvas id="canvas" :width="cWidth" :height="cHeight"></canvas>
    <div class="draw-btn-group">
      <div class="shape-box">
        <div
          :class="{ active: state.drawType == 'rectangle' }"
          class="shape-border"
          title="画矩形"
          @click="drawTypeChange('rectangle')">
          <i class="draw-icon draw-rect"></i>
        </div>
        <span>长方形</span>
      </div>
      <div class="shape-box">
        <div
          :class="{ active: state.drawType == 'polygon' }"
          class="shape-border shape-border-ti"
          title="画多边形"
          @click="drawPolygon('polygon')">
          <i class="draw-icon draw-rect"></i>
        </div>
        <span>不规则四边形</span>
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { fabric } from "fabric";
import { LoadingOutlined } from "@ant-design/icons-vue";
import { reactive, watch, onMounted } from "vue";
import { sortPoints } from "@/utils";

const props = defineProps({
  src: {
    type: String,
    default: "",
  },
  area: {
    type: String,
    default: "",
  },
  width: {
    type: Number,
    default: 1920,
  },
  height: {
    type: Number,
    default: 1080,
  },
  cWidth: {
    type: Number,
    default: 960,
  },
  cHeight: {
    type: Number,
    default: 540,
  },
});
const state = reactive({
  loading: true,
  radio: 0.5,
  realRadio: 0.5,
  imgPoint: { x: 0, y: 0 },
  realPoint: { x: 0, y: 0 },
  canvas: {} as any,
  mouseFrom: { x: 0, y: 0 } as canvasPoint,
  mouseTo: { x: 0, y: 0 } as canvasPoint,
  drawType: "rectangle" as string, //当前绘制图像的种类
  drawWidth: 2, //笔触宽度
  color: "#E34F51", //画笔颜色
  drawingObject: null as any, //当前绘制对象
  moveCount: 1, //绘制移动计数器
  doDrawing: false as boolean, // 绘制状态
  rectPath: "" as string, //矩形绘制路径
  //polygon 相关参数
  polygonMode: false as boolean,
  pointArray: [] as canvasPoint[],
  lineArray: [] as canvasPoint[],
  activeShape: false as any,
  activeLine: "" as any,
  line: {} as canvasPoint,
});
watch(
  () => state.drawType,
  (value) => {
    state.canvas.selection = !value;
  }
);
watch(
  () => props.width,
  (value) => {
    state.canvas.setWidth(value);
  }
);
watch(
  () => props.height,
  (value) => {
    state.canvas.setHeight(value);
  }
);
const loadInit = () => {
  if (props.src == "") {
    return;
  }
  state.loading = true;
  state.canvas = new fabric.Canvas("canvas", {});
  state.canvas.selectionColor = "rgba(0,0,0,0.05)";
  state.canvas.on("mouse:down", mousedown);
  state.canvas.on("mouse:move", mousemove);
  state.canvas.on("mouse:up", mouseup);
  let imgElement = new Image();
  imgElement.src = props.src;
  imgElement.onload = () => {
    state.radio = props.cWidth / imgElement.width;

    state.realRadio = props.width / imgElement.width;

    state.imgPoint.x = Math.floor(imgElement.width / 2);
    state.imgPoint.y = Math.floor(imgElement.height / 2);

    state.realPoint.x = Math.floor(props.width / 2);
    state.realPoint.y = Math.floor(props.height / 2);
    let imgInstance = new fabric.Image(imgElement, {
      selectable: false,
      width: imgElement.width,
      height: imgElement.height,
      scaleX: state.radio,
      scaleY: state.radio,
    });
    state.canvas.add(imgInstance);
    drawImage();
    state.canvas.renderAll();
    state.loading = false;
  };
};
const drawImage = () => {
  if (props.area === "") {
    clearAll();
    return;
  }
  let points = props.area.split(",").map((item) => {
    let areas = item.split(";");
    return areas.map((ars, index) => {
      let arp = 0;
      let ar = Number(ars);
      if (index % 2 == 0) {
        let dx =
          Math.abs(
            state.realPoint.x > ar
              ? state.realPoint.x - ar
              : state.realPoint.x + ar
          ) / state.realRadio;
        let rdx = Math.abs(state.imgPoint.x - dx);
        arp = rdx;
      } else {
        let dy =
          Math.abs(
            state.realPoint.y > ar
              ? state.realPoint.y - ar
              : state.realPoint.y + ar
          ) / state.realRadio;
        let rdy = Math.abs(state.imgPoint.y - dy);
        arp = rdy;
      }
      return Number(arp) * state.radio;
    });
  });
  points.forEach((point) => {
    drawImageObj(point);
  });
};
const drawImageObj = (data) => {
  let path = "M ";
  let points = [] as any;
  let len = data.length / 2;
  for (let i = 0; i < len; i++) {
    let idx = i * 2;
    points.push({ x: data[idx], y: data[idx + 1] });
    path += `${data[idx]} ${data[idx + 1]} L `;
  }
  let canvasObject = null as any;
  if (
    points[0].y === points[1].y &&
    points[2].y === points[3].y &&
    points[0].x === points[3].x &&
    points[1].x - points[2].x
  ) {
    path = path.replace(/L\s$/g, "z");
    canvasObject = new fabric.Path(path, {
      left: data[0],
      top: data[1],
      stroke: state.color,
      selectable: false,
      strokeWidth: state.drawWidth,
      fill: "rgba(255, 255, 255, 0)",
      hasControls: false,
    });
  } else {
    canvasObject = new fabric.Polygon(points, {
      stroke: state.color,
      strokeWidth: state.drawWidth,
      fill: "rgba(255, 255, 255, 0)",
      opacity: 1,
      hasBorders: false,
      hasControls: false,
      evented: false,
    });
  }
  canvasObject["points"] = points;
  state.canvas.add(canvasObject);
};
const drawTypeChange = (e) => {
  state.drawType = e;
  state.canvas.skipTargetFind = !!e;
  if (e == "pen") {
    // isDrawingMode为true 才可以自由绘画
    state.canvas.isDrawingMode = true;
  } else {
    state.canvas.isDrawingMode = false;
  }
};
// 鼠标按下时触发
const mousedown = (e) => {
  // 记录鼠标按下时的坐标
  var xy = e.pointer || transformMouse(e.e.offsetX, e.e.offsetY);
  state.mouseFrom.x = xy.x;
  state.mouseFrom.y = xy.y;
  state.doDrawing = true;
  // 绘制多边形
  if (state.drawType !== "rectangle") {
    state.canvas.skipTargetFind = false;
    try {
      let len = state.drawType === "line" ? 2 : 4;
      if (state.polygonMode) {
        addPoint(e);
      }
      if (state.pointArray.length === len && state.polygonMode) {
        generatePolygon();
      }
    } catch (error) {
      console.log(error);
    }
  }
};
// 鼠标松开执行
const mouseup = (e) => {
  let xy = e.pointer || transformMouse(e.e.offsetX, e.e.offsetY);
  state.mouseTo.x = xy.x;
  state.mouseTo.y = xy.y;
  state.drawingObject = null;
  state.moveCount = 1;
  if (state.drawType != "polygon" && state.drawType != "line") {
    state.doDrawing = false;
  }
  // 设置只允许绘制一个
  // let canvasObj = state.canvas.getObjects();
  // if(canvasObj.length >2){
  //   state.canvas.remove(canvasObj[1])
  // }
};
//鼠标移动过程中已经完成了绘制
const mousemove = (e) => {
  if (state.moveCount % 2 && !state.doDrawing) {
    //减少绘制频率
    return;
  }
  state.moveCount++;
  var xy = e.pointer || transformMouse(e.e.offsetX, e.e.offsetY);
  if (xy.y >= 0 && xy.x <= props.cWidth && xy.y >= 0 && xy.y <= props.cHeight) {
    state.mouseTo.x = xy.x;
    state.mouseTo.y = xy.y;
    // 矩形
    if (state.drawType == "rectangle") {
      if (
        state.mouseFrom.x < state.mouseTo.x &&
        state.mouseFrom.y < state.mouseTo.y
      ) {
        drawing(e);
      } else {
        // clearAll();
      }
    }
    if (state.drawType !== "rectangle") {
      if (state.activeLine && state.activeLine.class == "line") {
        var pointer = state.canvas.getPointer(e.e);
        state.activeLine.set({ x2: pointer.x, y2: pointer.y });

        var points = state.activeShape.get("points");
        points[state.pointArray.length] = {
          x: pointer.x,
          y: pointer.y,
          zIndex: 1,
        };
        state.activeShape.set({
          points: points,
        });
        state.canvas.renderAll();
      }
      state.canvas.renderAll();
    }
  } else {
    // clearAll();
  }
};
// 绘制矩形
const drawing = (_event) => {
  if (state.drawingObject) {
    state.canvas.remove(state.drawingObject);
  }
  let canvasObject = null;
  let left = state.mouseFrom.x,
    top = state.mouseFrom.y,
    mouseFrom = state.mouseFrom,
    mouseTo = state.mouseTo;
  var path =
    "M " +
    mouseFrom.x +
    " " +
    mouseFrom.y +
    " L " +
    mouseTo.x +
    " " +
    mouseFrom.y +
    " L " +
    mouseTo.x +
    " " +
    mouseTo.y +
    " L " +
    mouseFrom.x +
    " " +
    mouseTo.y +
    " L " +
    mouseFrom.x +
    " " +
    mouseFrom.y +
    " z";
  state.rectPath = path;
  canvasObject = new fabric.Path(path, {
    left: left,
    top: top,
    stroke: state.color,
    selectable: false,
    strokeWidth: state.drawWidth,
    fill: "rgba(255, 255, 255, 0)",
    hasControls: false,
  });
  if (canvasObject) {
    state.canvas.add(canvasObject);
    state.drawingObject = canvasObject;
  }
};
// 绘制多边形开始,绘制多边形和其他图形不一样,需要单独处理
const drawPolygon = (type) => {
  state.drawType = type;
  state.polygonMode = true;
  //这里画的多边形,由顶点与线组成
  state.pointArray = new Array(); // 顶点集合
  state.lineArray = new Array(); //线集合
  state.canvas.isDrawingMode = false;
};
const addPoint = (e) => {
  let random = Math.floor(Math.random() * 10000);
  let id = new Date().getTime() + random;
  let circle = new fabric.Circle({
    radius: 5,
    fill: "#ffffff",
    stroke: "#333333",
    strokeWidth: 0.5,
    left: (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
    top: (e.pointer.y || e.e.layerY) / state.canvas.getZoom(),
    selectable: false,
    hasBorders: false,
    hasControls: false,
    originX: "center",
    originY: "center",
    id: id,
    objectCaching: false,
  });
  if (state.pointArray.length == 0) {
    circle.set({
      fill: "#00FFFF",
    });
  }
  var points = [
    (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
    (e.pointer.y || e.e.layerY) / state.canvas.getZoom(),
    (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
    (e.pointer.y || e.e.layerY) / state.canvas.getZoom(),
  ];

  state.line = new fabric.Line(points, {
    strokeWidth: 2,
    fill: "#999999",
    stroke: "#999999",
    class: "line",
    originX: "center",
    originY: "center",
    selectable: false,
    hasBorders: false,
    hasControls: false,
    evented: false,

    objectCaching: false,
  });
  if (state.activeShape) {
    let pos = state.canvas.getPointer(e.e);
    let points = state.activeShape.get("points");
    points.push({
      x: pos.x,
      y: pos.y,
    });
    var polygon = new fabric.Polygon(points, {
      stroke: "#333333",
      strokeWidth: 1,
      fill: "#cccccc",
      opacity: 0.3,
      selectable: false,
      hasBorders: false,
      hasControls: false,
      evented: false,
      objectCaching: false,
    });
    state.canvas.remove(state.activeShape);
    state.canvas.add(polygon);
    state.activeShape = polygon;
    state.canvas.renderAll();
  } else {
    var polyPoint = [
      {
        x: (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
        y: (e.pointer.y || e.e.layerY) / state.canvas.getZoom(),
      },
    ];
    var polygon = new fabric.Polygon(polyPoint, {
      stroke: "#333333",
      strokeWidth: 1,
      fill: "#cccccc",
      opacity: 0.3,
      selectable: false,
      hasBorders: false,
      hasControls: false,
      evented: false,
      objectCaching: false,
    });
    state.activeShape = polygon;
    state.canvas.add(polygon);
  }
  state.activeLine = state.line;

  state.pointArray.push(circle);
  state.lineArray.push(state.line);
  state.canvas.add(state.line);
  state.canvas.add(circle);
};
// 绘制不规则四边形
const generatePolygon = () => {
  let points = clearPolygonLines();
  var polygon = new fabric.Polygon(sortPoints(points), {
    stroke: state.color,
    strokeWidth: state.drawWidth,
    fill: "rgba(255, 255, 255, 0)",
    opacity: 1,
    hasBorders: false,
    hasControls: false,
    evented: false,
  });
  state.canvas.add(polygon);
  resetPolygon();
};
// 坐标转换
const transformMouse = (mouseX, mouseY) => {
  return { x: mouseX / 1, y: mouseY / 1 };
};
// 重置不规则四边形
const resetPolygon = () => {
  state.activeLine = null;
  state.activeShape = null;
  state.polygonMode = false;
  state.doDrawing = false;
  state.drawType = "rectangle";
};
// 清除绘制四边形的四个坐标点
const clearPolygonLines = () => {
  let points = new Array();
  state.pointArray.forEach((point) => {
    points.push({
      x: point.left,
      y: point.top,
    });
    state.canvas.remove(point);
  });
  state.lineArray.forEach((line) => {
    state.canvas.remove(line);
  });
  state.canvas.remove(state.activeShape).remove(state.activeLine);
  return points;
};
// 撤销最后一次的操作
const clearObjLast = () => {
  let canvasObjs = state.canvas.getObjects();
  let len = canvasObjs.length;
  if (len > 1) {
    state.canvas.remove(canvasObjs[len - 1]);
  }
};
// 全部清除
const clearAll = () => {
  state.canvas.getObjects().forEach((element, index) => {
    if (index > 0) {
      state.canvas.remove(element);
    }
  });
  clearPolygonLines();
  resetPolygon();
  state.drawType = "rectangle";
};
const getPoint = (pi) => {
  return Math.floor(pi / state.radio);
};
const getRealPoint = (poi) => {
  let dx =
    Math.abs(
      state.imgPoint.x > poi.x
        ? state.imgPoint.x - poi.x
        : state.imgPoint.x + poi.x
    ) * state.realRadio;
  let dy =
    Math.abs(
      state.imgPoint.y > poi.y
        ? state.imgPoint.y - poi.y
        : state.imgPoint.y + poi.y
    ) * state.realRadio;
  let rdx = Math.abs(state.realPoint.x - dx);
  let rdy = Math.abs(state.realPoint.y - dy);
  let minX = Math.min(Math.floor(rdx), props.width);
  let minY = Math.min(Math.floor(rdy), props.height);
  return { x: minX, y: minY };
};
// 生成真实分辨率图片的坐标点
const getData = () => {
  let datas = [] as any;
  let marks = ["tl", "tr", "br", "bl"];
  if (state.lineArray.length > 0 && state.lineArray.length < 4) {
    clearPolygonLines();
  }
  state.canvas.getObjects().forEach((item, index) => {
    if (index > 0) {
      let aCoords = item.aCoords;
      let point = {};
      if (item.points) {
        item.points.forEach((item, idx) => {
          if (aCoords[marks[idx]]) {
            aCoords[marks[idx]].x = item.x;
            aCoords[marks[idx]].y = item.y;
          }
        });
      }
      marks.forEach((mark) => {
        let poi = {
          x: getPoint(aCoords[mark].x),
          y: getPoint(aCoords[mark].y),
        };
        point[mark] = getRealPoint(poi);
      });
      if (item.points && item.points.length === 2) {
        delete point["br"];
        delete point["bl"];
      }
      datas.push(point);
    }
  });
  return datas;
};
defineExpose({ drawType: state.drawType, clearObjLast, clearAll, getData });
onMounted(() => {
  loadInit();
});
</script>

<style lang="less" scoped>
canvas {
  border: 1px dashed black;
}
.loading-box {
  width: 960px;
  height: 540px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  font-size: 14px;
  color: #ea5413;
}
.draw-btn-group {
  width: 960px;
  margin-top: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  .active {
    .draw-rect {
      background: #ff00ff;
      border-color: #ff00ff;
    }
  }
  .shape-box {
    text-align: left;
    width: 120px;
  }
  .shape-border {
    display: block;
    width: 80px;
    height: 30px;
    text-align: center;
    font-size: 12px;
    margin-right: 30px;
  }
  .shape-border-ti {
    transform: skewX(-45deg);
  }
  .draw-icon {
    display: inline-block;
    width: 80px;
    height: 30px;
  }
  .draw-rect {
    width: 80px;
    border-width: 1px;
    border-style: solid;
    border-color: #333;
  }

  .draw-line {
    position: relative;
    top: -14px;
    border-bottom: 2px solid #00ffff;
  }
}
</style>

封装到Modal中

//新建文件img-detect.vue
<template>
  <a-modal
    class="modal-canvas"
    v-model:visible="visible"
    title="添加区域"
    width="980px"
    height="600px"
    :footer="null">
    <ImgMaker
      ref="imgMaker"
      v-if="visible"
      :src="cover"
      :area="area"
      :width="width"
      :height="height"></ImgMaker>
    <div style="text-align: center">
      <a-space>
        <a-button type="primary" @click="onClearLast">撤销最后一次的操作</a-button>
        <a-button type="primary" @click="onClearAll">清空所有</a-button>
        <a-button type="primary" @click="onCancel">取消</a-button>
        <a-button type="primary" @click="onSubmit">保存</a-button>
      </a-space>
    </div>
  </a-modal>
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs } from "vue";
import ImgMaker from "./img-maker.vue";
const props = defineProps({
  area: {
    type: String,
    default: "",
  },
  img: {
    type: String,
    default: "",
  },
  width: {
    type: Number,
    default: 1920,
  },
  height: {
    type: Number,
    default: 1080,
  },
});
const emit = defineEmits(["success"]);
const imgMaker = ref();
const state = reactive({
  visible: false,
  cover: "",
});
const { visible, cover } = toRefs(state);
state.cover = props.img;
const onClearAll = () => {
  imgMaker.value.clearAll();
};
const onClearLast = () => {
  imgMaker.value.clearObjLast();
};
const onCancel = () => {
  onClearAll();
  state.visible = false;
  imgMaker.value.drawType = "rectangle";
};
const onSubmit = () => {
  let points = imgMaker.value.getData();
  let datas = [];
  if (points.length > 0) {
    datas = points.map((point) => {
      if (point.br) {
        return `${point.tl.x};${point.tl.y};${point.tr.x};${point.tr.y};${point.br.x};${point.br.y};${point.bl.x};${point.bl.y}`;
      }
      return `${point.tl.x};${point.tl.y};${point.tr.x};${point.tr.y}`;
    });
  }
  emit("success", datas.join(","));
  onClearAll();
  state.visible = false;
};
defineExpose({ visible, cover });
</script>

进行调用

//新建文件index.vue
<template>
  <img :src="cover" alt="" style="width: 576px; height: 324px" />
  <div style="margin: 10px 0">
    <a-space>
      <a-button type="primary" @click="handleDelArea">删除所有</a-button>
      <a-button type="primary" @click="handleArea">添加</a-button>
    </a-space>
  </div>
  <div>区域坐标:{{ area.split(",") }}</div>

  <img-detect
    ref="imgDetect"
    :img="cover"
    :area="area"
    :width="width"
    :height="height"
    @success="handleImgSuccess"></img-detect>
</template>
<script lang="ts" setup>
import ImgDetect from "@/components/img-detect.vue";
import { message } from "ant-design-vue";
import { reactive, ref, toRefs } from "vue";
import { useDrawArea } from "@/hooks/useDrawArea";
const state = reactive({
  area: "" as string,
  width: 1920 as number,
  height: 1080 as number,
  cover:"/public/a.jpg",
});

const imgDetect = ref();
const getList = () => {
  state.cover = "/public/a.jpg";
  useDrawArea({
    src: state.cover,
    width: state.width,
    height: state.height,
    area: state.area,
  })
    .then((url) => {
      state.cover = url as string;
    })
    .catch(() => {});
};

const handleArea = () => {
  if (state.cover != "") {
    imgDetect.value.visible = true;
  } else {
    message.warning("缺失图像,暂时不能进行区域添加!");
  }
};
const handleDelArea = () => {
  state.area = "";
  getList();
};
const handleImgSuccess = (data) => {
  state.area = data;
  getList()
};
const { area, width, height, cover } = toRefs(state);
</script>
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值