使用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>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix