使用fabric.js框选图片区域定位标注图片内容

仍然是 在图片上特定区域根据数值显示不同的颜色 的需求,过了这么久,svg图迟迟提供不了,考虑canvas方案。记录比较下canvas及各canvas框架的使用。

canvas

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>使用 JavaScript 在图像上选择区域</title>
  </head>
  <body>
    <div>
      <canvas id="canvas1"></canvas>
      <script>
        {
          const canvas = document.getElementById("canvas1");
          const ctx = canvas.getContext("2d");

          const image = new Image();
          image.src = "../images/1.jpg";

          image.onload = function () {
            canvas.width = image.width;
            canvas.height = image.height;
            ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
          };

          let startX;
          let startY;
          let width;
          let height;

          canvas.addEventListener("mousedown", function (event) {
            startX = event.offsetX;
            startY = event.offsetY;
          });

          canvas.addEventListener("mouseup", function (event) {
            if (startX !== undefined && startY !== undefined) {
              width = event.offsetX - startX;
              height = event.offsetY - startY;

              ctx.beginPath();
              ctx.rect(startX, startY, width, height);
              ctx.strokeStyle = "#FF0000";
              ctx.stroke();

              console.log(`选中区域的位置为 (${startX}, ${startY}),大小为 ${width} × ${height}`);
            }
            startX = undefined;
            startY = undefined;
            width = undefined;
            height = undefined;
          });
        }
      </script>
    </div>
  </body>
</html>

使用鼠标在图片上拖动来绘制矩形及其他图形区域

annotorious.js

https://github.com/annotorious/annotorious
<link rel="stylesheet" href="../plugins/annotorious-2.7.13/annotorious.min.css" />
<script src="../plugins/annotorious-2.7.13/annotorious.min.js"></script>
<script src="../plugins/annotorious-2.7.13/annotorious-toolbar.min.js"></script>
<style>
  #hallstatt {
    border: 1px solid red;
  }
</style>
<div id="toolbar"></div>
<img id="hallstatt" src="../images/1.jpg" />

<script>
  var sampleAnnotation = {
    "@context": "http://www.w3.org/ns/anno.jsonld",
    id: "#a88b22d0-6106-4872-9435-c78b5e89fede",
    type: "Annotation",
    body: [
      {
        type: "TextualBody",
        value: "It's Hallstatt in Upper Austria",
      },
      {
        type: "TextualBody",
        purpose: "tagging",
        value: "Hallstatt",
      },
      {
        type: "TextualBody",
        purpose: "tagging",
        value: "Upper Austria",
      },
    ],
    target: {
      selector: {
        type: "FragmentSelector",
        conformsTo: "http://www.w3.org/TR/media-frags/",
        value: "xywh=pixel:421,80,151,151",
      },
    },
  };

  window.onload = function () {
    var anno = Annotorious.init({
      image: "hallstatt",
      locale: "auto",
      allowEmpty: true,
    });

    Annotorious.Toolbar(anno, document.getElementById("toolbar"));

    //可以标注矩形和多边形区域,不能标注圆形区域,且添加的注释未显示在图片上
    anno.on("createAnnotation", function (annotation) {
      console.log(annotation.target, annotation.body);
    });
  };
</script>

fabric.js

https://github.com/fabricjs/fabric.js

<script src="../plugins/fabric.min.js"></script>
<link href="../plugins/bootstrap-5.1.3/css/bootstrap.min.css" rel="stylesheet" />
<script src="../plugins/jquery/jquery-3.3.1.js"></script>
<script src="../plugins/bootstrap-5.1.3/js/bootstrap.bundle.min.js"></script>
<style>
  #tooltip {
    position: absolute;
    display: none;
    background-color: white;
    border: 1px solid silver;
    box-shadow: 0 0 5px grey;
    border-radius: 3px;
  }
  #tooltip div {
    display: inline-block;
    padding: 0.25rem 0.5rem;
  }
  #tooltip div span:last-child {
    font-weight: bold;
    margin-left: 2rem;
  }
</style>
<body>
  <div class="row g-2">
    <div class="col-2">
      <ul class="nav nav-tabs mt-2" id="myTab">
        <li class="nav-item">
          <a class="nav-link active" data-bs-toggle="tab" data-bs-target="#shape-rect" style="cursor: pointer">矩形</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" data-bs-toggle="tab" data-bs-target="#shape-ellipse" style="cursor: pointer">椭圆形</a>
        </li>
      </ul>
      <div class="tab-content">
        <div class="tab-pane fade active show" id="shape-rect" role="tabpanel">
          <div class="row g-2">
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">Left</span>
                <input type="number" min="0" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">Top</span>
                <input type="number" min="0" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">宽度</span>
                <input type="number" min="0" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">高度</span>
                <input type="number" min="0" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">名字</span>
                <input type="text" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">数量</span>
                <input type="number" min="1" class="form-control" />
              </div>
            </div>
          </div>
          <button class="btn btn-secondary mt-2" onclick="addRect()">新增</button>
        </div>
        <div class="tab-pane fade" id="shape-ellipse" role="tabpanel">
          <div class="row g-2">
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">Left</span>
                <input type="number" min="0" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">Top</span>
                <input type="number" min="0" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">水平半径</span>
                <input type="number" min="1" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">垂直半径</span>
                <input type="number" min="1" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">线宽</span>
                <input type="number" min="1" class="form-control" />
              </div>
            </div>
            <div class="col-12">
              <div class="input-group">
                <span class="input-group-text">名字</span>
                <input type="text" class="form-control" />
              </div>
            </div>
          </div>
          <button class="btn btn-secondary mt-2" onclick="addEllipse()">新增</button>
        </div>
      </div>

      <hr />
      <div class="input-group mb-2">
        <span class="input-group-text">名字</span>
        <input type="text" id="object_name" class="form-control" />
      </div>
      <button type="button" class="btn btn-secondary" onclick="setObjectName()">修改名称</button>

      <hr />
      <button class="btn btn-secondary mb-2" onclick="cloneShape()">复制</button>
      <button class="btn btn-secondary mb-2" onclick="delShape()">删除</button>
      <button class="btn btn-secondary mb-2" onclick="getAllShape()">预览</button>

      <hr />
      <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#note">使用说明</button>
      <div>
        水平距离:<span id="distince_x"></span>
        <br />
        垂直距离:<span id="distince_y"></span>
      </div>
      <div><span id="info">0</span></div>
    </div>
    <div class="col-10" style="overflow: auto">
      <canvas id="example" style="border: solid 1px #ccc"></canvas>
      <canvas id="example_re" style="border: solid 1px #ccc"></canvas>
    </div>
  </div>

  <div class="modal fade" tabindex="-1" id="note" tabindex="-1">
    <div class="modal-dialog modal-lg">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">使用说明</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          1 本页面用于标识区域的位置及尺寸,仅支持标示矩形和椭圆形区域,支持添加、复制、删除和预览 <br />
          <p class="mb-0 ps-4">
            使用时页面禁止缩放,否则可能造成位置及尺寸不准<br />
            新增图形可以使用表单指定区域的位置及尺寸,矩形区域可以批量添加<br />
            可以使用鼠标选中多个图形移动或者缩放<br />
            可以使用键盘上下左右键来调整图形的位置<br />
            使用鼠标选中某个图形时,图形的名称信息会显示在左侧表单中,修改表单中名称信息,图形tooltip会随之变化<br />
          </p>
          2 矩形区域可批量添加<br />
          <p class="mb-0 ps-4">
            Left、Top、宽度、高度参数分别表示距离图片左上角的水平距离、垂直距离、矩形区域宽度、矩形区域高度<br />
            名字参数表示当前矩形区域的名字<br />
            数量参数用于批量添加矩形区域<br />
          </p>
          3 椭圆形区域<br />
          <p class="mb-0 ps-4">
            Left、Top、水平半径、垂直半径参数分别表示距离图片左上角的水平距离、垂直距离、椭圆形的水平方向半径和垂直方向半径<br />
            线宽参数值为1时图形为填充圆形,<br />
            线宽参数值大于1时为圆环,圆环区域的外半径与内半径差为LineWidth参数值,此时水平半径和垂直半径应为外半径和内半径的均值。<br />
          </p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>

  <div id="tooltip">
    <div>
      <span>Name</span>
      <span>aa</span>
    </div>
  </div>

  <script>
    const img_path = "../images/2.jpg";
    const canvas = new fabric.Canvas("example");
    canvas.setBackgroundImage(img_path, canvas.renderAll.bind(canvas)); //背景图
    const canvas_re = new fabric.Canvas("example_re");
    // const canvas_re = new fabric.StaticCanvas("example_re");//无交互
    const tooltip = document.getElementById("tooltip");
    let canvas_params, canvas_re_params;
    let gbl_coord_x = 0;
    let gbl_coord_y = 0;

    //通用属性
    const default_prop = {
      transparentCorners: false, //选中时 控制手柄的样式
      borderColor: "green",
      cornerColor: "green",
      cornerSize: 5,
      lockRotation: true, //禁止旋转
      lockScalingFlip: true, //禁止缩放时翻转
      lockSkewingX: true, //禁止水平方向扭曲
      lockSkewingY: true, //禁止垂直方向扭曲
    };
    const default_vals_rect = [0, 0, 10, 20, "rect", 1];
    const default_vals_ellipse = [0, 0, 20, 20, 5, "ellipse"];
    $("#shape-rect input").each((idx, elem) => {
      elem.value = default_vals_rect[idx];
    });
    $("#shape-ellipse input").each((idx, elem) => {
      elem.value = default_vals_ellipse[idx];
    });

    // 创建对象
    const bg_img = new Image();
    bg_img.src = img_path;
    bg_img.onload = function () {
      //将canvas的宽高设置为背景图片的宽高
      canvas.setWidth(bg_img.width);
      canvas.setHeight(bg_img.height);
      canvas_params = document.querySelector("#example").getBoundingClientRect(); //元素的位置宽高等信息

      canvas_re.setWidth(bg_img.width);
      canvas_re.setHeight(bg_img.height);
      canvas_re_params = document.querySelector("#example_re").getBoundingClientRect();
    };

    //根据参数值或者表单值添加矩形区域
    function addRect() {
      const total_width = canvas.getWidth();
      const total_height = canvas.getHeight();

      const vals = [];
      let flag = true;
      const elems = $("#shape-rect input");
      for (let i = 0; i < elems.length - 1; i++) {
        const val = elems[i].value.trim();
        if (val.length < 1 || (i < 4 && isNaN(val)) || parseInt(val) < 0) {
          flag = false;
          alert("各参数不能为空,前4项值应为正整数");
          break;
        } else if (vals[0] > total_width || vals[0] + vals[2] >= total_width || vals[1] > total_height || vals[1] + vals[3] >= total_height) {
          flag = false;
          alert("请检查参数值");
          break;
        }
        vals.push(i < 4 ? parseInt(val) : val);
      }

      let num = $("#shape-rect input:eq(5)").val().trim();
      num = isNaN(num) ? 1 : parseInt(num);
      const c_width = vals[2] + 10;
      const c_height = vals[3] + 10;
      const col_num_max = Math.floor(total_width / c_width);
      const row_num_max = Math.floor(total_height / c_height);

      let count = 0;
      for (let i = 0; i < row_num_max; i++) {
        for (let j = 0; j < col_num_max; j++) {
          count++;
          if (count <= num) {
            const left = 5 + j * c_width;
            const top = 5 + i * c_height;
            const rect = new fabric.Rect(
              Object.assign(
                {
                  left: left,
                  top: top,
                  originX: "left",
                  originY: "top",
                  width: vals[2],
                  height: vals[3],
                  fill: "rgba(255, 0, 0, 0.5)",
                },
                default_prop
              )
            );
            rect.set("name", vals[4]); //自定义属性
            canvas.add(rect);
          }
        }
      }
    }

    //根据参数值或者表单值添加椭圆形区域
    function addEllipse() {
      let flag = true;
      const vals = [];
      const elems = $("#shape-ellipse input");
      for (let i = 0; i < elems.length; i++) {
        const val = elems[i].value.trim();
        if (val.length < 1 || (i < 5 && isNaN(val)) || parseInt(val) < 0) {
          flag = false;
          alert("各参数不能为空,前4项值应为正整数");
          break;
        }
        vals.push(i < 5 ? parseInt(val) : val);
      }

      if (vals[3] > 1) {
        //模拟圆环
        const ellipse = new fabric.Ellipse(
          Object.assign(
            {
              left: vals[0],
              top: vals[1],
              rx: vals[2],
              ry: vals[3],
              fill: "transparent",
              strokeWidth: vals[4],
              stroke: "rgba(255, 0, 0, 0.5)",
            },
            default_prop
          )
        );
        ellipse.set("name", vals[5]); //自定义属性
        canvas.add(ellipse);
      } else {
        const ellipse = new fabric.Ellipse(
          Object.assign(
            {
              left: vals[0],
              top: vals[1],
              rx: vals[2],
              ry: vals[3],
              fill: "rgba(255, 0, 0, 0.5)",
            },
            default_prop
          )
        );
        ellipse.set("name", vals[5]);
        canvas.add(ellipse);
      }
    }

    //将名字的变化 同步到canvas上
    function setObjectName() {
      const active_obj = canvas.getActiveObject();
      if (active_obj.type != "activeSelection" && active_obj instanceof fabric.Object) {
        active_obj.set("name", $("#object_name").val());
        canvas.renderAll();
      }
      $("#object_name").val("");
    }

    //复制选中的对象
    function cloneShape() {
      if (typeof canvas.getActiveObject() === "undefined") return;

      let pos_x = 0;
      let pos_y = 0;
      //选择多个对象时需要重新计算坐标位置
      if (canvas.getActiveObject().type == "activeSelection") {
        const activeSelection = canvas.getActiveObject();
        pos_x = (activeSelection.left + (activeSelection.left + activeSelection.width)) / 2;
        pos_y = (activeSelection.top + (activeSelection.top + activeSelection.height)) / 2;
      }

      const active_objs = canvas.getActiveObjects();
      for (let i in active_objs) {
        const active_obj = active_objs[i];
        if (active_obj instanceof fabric.Object) {
          const left = pos_x + active_obj.get("left") + 20;
          const top = pos_y + active_obj.get("top") + 20;

          active_obj.clone((clone) => {
            Object.assign(clone, default_prop);
            clone.set("name", active_obj.get("name"));
            clone.set("left", left);
            clone.set("top", top);
            canvas.add(clone);
          });
        }
      }
    }

    //删除选中的对象
    function delShape() {
      const active_objs = canvas.getActiveObjects();
      for (let i in active_objs) {
        const active_obj = active_objs[i];
        if (active_obj instanceof fabric.Object) {
          canvas.remove(active_obj);
        }
      }
    }

    //获取canvas上的所有对象数据,在另外的canvas上重绘预览
    function getAllShape() {
      canvas.discardActiveObject();

      const data = [];
      const all_obj = canvas.getObjects();

      const data_json = canvas.toJSON(["name"]); //转换时包含自定义属性name
      canvas_re.loadFromJSON(data_json, canvas_re.renderAll.bind(canvas_re), function (o, obj) {
        if (obj.type == "rect") {
          obj.set({ fill: getCorlor(obj.get("name")) });
        } else if (obj.type == "ellipse") {
          if (obj.get("strokeWidth") > 1) {
            obj.set({ stroke: getCorlor(obj.get("name")) });
          } else {
            obj.set({ fill: getCorlor(obj.get("name")) });
          }
        }
      });

      // console.log(canvas_re.getObjects());//结果为空,不明白why
      document.querySelector("#info").innerHTML = data_json["objects"].length;
    }

    //根据自定义属性name的值填入颜色
    function getCorlor(name) {
      if (name == "11") {
        return "rgba(0, 0, 255, 0.5)";
      } else if (name == "12") {
        return "rgba(0, 255, 0, 0.5)";
      } else {
        //渐变色
        const gradient = new fabric.Gradient({
          type: "linear",
          gradientUnits: "percentage",
          coords: { x1: 0, y1: 0, x2: 1, y2: 0 },
          colorStops: [
            { offset: 0, color: "red" },
            { offset: 1, color: "blue" },
          ],
        });
        return gradient;
      }
    }

    //tooltip
    canvas.on("mouse:over", function (opt) {
      console.log("canvas", opt);
      console.log(canvas_params, canvas.getWidth(), canvas.getHeight());

      if (opt.target && opt.target.type !== "activeSelection") {
        $("#tooltip div span:last-child").text(opt.target["name"]);
        const values = opt.target.getBoundingRect();
        tooltip.style.display = "initial";
        tooltip.style.top = canvas_params["top"] + values["top"] + Math.floor(values["height"] / 2) + 5 + "px";
        tooltip.style.left = canvas_params["left"] + values["left"] + Math.floor(values["width"] / 2) + 5 + "px";
      }
    });
    canvas.on("mouse:out", function (opt) {
      tooltip.style.display = "none";
    });

    canvas_re.on("mouse:over", function (opt) {
      console.log("canvas_re", opt);
      console.log(canvas_re_params, canvas.getWidth(), canvas.getHeight());
      if (opt.target && opt.target.type !== "activeSelection") {
        $("#tooltip div span:last-child").text(opt.target["name"]);
        const values = opt.target.getBoundingRect();
        tooltip.style.display = "initial";
        tooltip.style.top = canvas_re_params["top"] + values["top"] + Math.floor(values["height"] / 2) + 5 + "px";
        tooltip.style.left = canvas_re_params["left"] + values["left"] + Math.floor(values["width"] / 2) + 5 + "px";
      }
    });
    canvas_re.on("mouse:out", function (opt) {
      tooltip.style.display = "none";
    });

    //选中事件
    canvas.on("selection:created", function (opt) {
      const active_obj = canvas.getActiveObject();
      if (active_obj.type != "activeSelection") {
        $("#object_name").val(active_obj.get("name"));
      } else {
        $("#object_name").val("");
      }
    });
   //选中事件
    canvas.on("selection:updated", function (opt) {
      const active_obj = canvas.getActiveObject();
      if (active_obj.type != "activeSelection") {
        $("#object_name").val(active_obj.get("name"));
      } else {
        $("#object_name").val("");
      }
    });
    //取消选中事件
    canvas.on("selection:cleared", function (opt) {
      $("#object_name").val("");
    });

    //测量图片上两点的水平距离和垂直距离
    canvas.on("mouse:down", function (opt) {
      $("#distince_x").text(Math.abs(opt.pointer.x - gbl_coord_x).toFixed(1));
      $("#distince_y").text(Math.abs(opt.pointer.y - gbl_coord_y).toFixed(1));
      gbl_coord_x = opt.pointer.x;
      gbl_coord_y = opt.pointer.y;
    });

    // 监听键盘事件,主要用于对齐位置
    document.addEventListener("keydown", function (event) {
      const active_objs = canvas.getActiveObjects();

      for (let i in active_objs) {
        const active_obj = active_objs[i];
        if (active_obj instanceof fabric.Object) {
          switch (event.keyCode) {
            case 37: // 左键
              active_obj.set({ left: active_obj.get("left") - 1 });
              // canvas.renderAll();
              break;
            case 38: // 上键
              active_obj.set({ top: active_obj.get("top") - 1 });
              // canvas.renderAll();
              break;
            case 39: // 右键
              active_obj.set({ left: active_obj.get("left") + 1 });
              // canvas.renderAll();
              break;
            case 40: // 下键
              active_obj.set({ top: active_obj.get("top") + 1 });
              // canvas.renderAll();
              break;
            default:
              break;
          }
        }
      }
      canvas.renderAll();
    });
  </script>
</body>

 

posted @   carol2014  阅读(764)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示