仍然是 在图片上特定区域根据数值显示不同的颜色 的需求,改进下代码。
增加了测量辅助线、对齐辅助线、生成svg图等,基本满足需求。
demo中包括了生成json、svg字符串和下载svg图。
<script src="../plugins/fabric.min.js"></script>
<script src="../plugins/aligning_guidelines.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;
}
#genSvg {
border: 1px solid silver;
}
</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">宽度</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">外水平半径</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="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 class="my-1" />
<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 class="my-1" />
<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 class="my-1" />
<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 id="genSvg"></div>
</div>
</div>
<div id="tooltip">
<div>
<span>Name</span>
<span></span>
</div>
</div>
<script>
const img_path = "../images/2.jpg";
const canvas = new fabric.Canvas("example");
canvas.setBackgroundImage(img_path, canvas.renderAll.bind(canvas)); //背景图
initAligningGuidelines(canvas); //用于对齐的辅助线
const canvas_re = new fabric.Canvas("example_re");
// const canvas_re = new fabric.StaticCanvas("example_re");//无交互
canvas_re.selection = false;
const tooltip = document.getElementById("tooltip");
let canvas_params, canvas_re_params;
let gbl_coord_x = 0;
let gbl_coord_y = 0;
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(); //元素的位置宽高等信息
//辅助线
const line_h = new fabric.Line([0, 3, canvas.getWidth(), 3], {
stroke: "silver",
strokeWidth: 2,
excludeFromExport: true, //不导出
strokeDashArray: [5, 5], //虚线
lockRotation: true,
lockScalingFlip: true,
lockSkewingX: true,
lockSkewingY: true,
lockScalingX: true,
lockScalingY: true,
});
line_h.set("name", "auxiliary_line_h"); //自定义属性
const line_v = new fabric.Line([3, 0, 3, canvas.getHeight()], {
stroke: "silver",
strokeWidth: 2,
excludeFromExport: true,
strokeDashArray: [5, 5],
lockRotation: true,
lockScalingFlip: true,
lockSkewingX: true,
lockSkewingY: true,
lockScalingX: true,
lockScalingY: true,
});
line_v.set("name", "auxiliary_line_v"); //自定义属性
canvas.add(line_h);
canvas.add(line_v);
canvas_re.setWidth(bg_img.width);
canvas_re.setHeight(bg_img.height);
canvas_re_params = document.querySelector("#example_re").getBoundingClientRect();
};
//通用属性
const default_prop = {
transparentCorners: false, //选中时 控制手柄的样式
borderColor: "green",
cornerColor: "green",
cornerSize: 5,
lockRotation: true, //禁止旋转
lockScalingFlip: true, //禁止缩放时翻转
lockSkewingX: true, //禁止水平方向扭曲
lockSkewingY: true, //禁止垂直方向扭曲
};
const default_vals_rect = [20, 50, "rect", 1];
const default_vals_ellipse = [75, 75, 50, 50, "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];
});
//根据参数值或者表单值添加矩形区域
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 != 2 && isNaN(val)) || parseInt(val) < 0) {
flag = false;
alert("各参数不能为空,第1、2、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 < 2 ? parseInt(val) : val);
}
let num = $("#shape-rect input:eq(3)").val().trim();
num = isNaN(num) ? 1 : parseInt(num);
const c_width = vals[0] + 10;
const c_height = vals[1] + 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[0],
height: vals[1],
fill: "rgba(255, 0, 0, 0.5)",
},
default_prop
)
);
rect.set("name", vals[2]); //自定义属性
canvas.add(rect);
rewriteToSvg(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 != 4 && isNaN(val)) || parseInt(val) < 0) {
flag = false;
alert("各参数不能为空,前4项值应为正整数");
break;
}
vals.push(i < 4 ? parseInt(val) : val);
}
if (vals[2] >= vals[0] || vals[3] >= vals[1]) {
vals[2] = 0;
vals[3] = 0;
}
if (vals[2] > 1 && vals[3] > 1) {
//模拟圆环-导出svg显示圆环
const ellipse = new fabric.Ellipse({
left: 0,
top: 0,
rx: vals[0],
ry: vals[1],
fill: "rgba(255, 0, 0, 0.5)",
});
const ellipse_inner = new fabric.Ellipse({
rx: vals[2],
ry: vals[3],
left: vals[0] - vals[2],
top: vals[1] - vals[3],
fill: "rgba(255, 255, 255, 0.9)",
});
const group = new fabric.Group(
[ellipse, ellipse_inner],
Object.assign(
{
left: 0,
top: 0,
},
default_prop
)
);
ellipse.set("name", vals[4]); //自定义属性
canvas.add(group);
rewriteToSvg(ellipse);
} else {
const ellipse = new fabric.Ellipse(
Object.assign(
{
left: 0,
top: 0,
rx: vals[0],
ry: vals[1],
fill: "rgba(255, 0, 0, 0.5)",
},
default_prop
)
);
ellipse.set("name", vals[4]);
canvas.add(ellipse);
rewriteToSvg(ellipse);
}
}
//将名字的变化 同步到canvas上
function setObjectName() {
const active_obj = canvas.getActiveObject();
if (typeof active_obj == "undefined" || active_obj == null) return;
if (active_obj.type != "activeSelection" && active_obj instanceof fabric.Object) {
if (active_obj.type != "group") {
active_obj.set("name", $("#object_name").val());
} else {
//圆环由两个圆组成,只设置外圆
const objects = active_obj.getObjects();
objects[0].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;
if (active_obj.type == "group") {
active_obj.clone((clone) => {
const objects = active_obj.getObjects();
const objects_clone = clone.getObjects();
objects_clone[0].set("name", objects[0].get("name"));
clone.set("left", left);
clone.set("top", top);
clone.set(default_prop);
canvas.add(clone);
rewriteToSvg(clone);
});
} else {
active_obj.clone((clone) => {
clone.set("name", active_obj.get("name"));
clone.set("left", left);
clone.set("top", top);
clone.set(default_prop);
canvas.add(clone);
rewriteToSvg(clone);
});
}
}
}
}
//重写toSVG方法,使生成的图形带上自定义属性name
function rewriteToSvg(obj) {
obj.toSVG = (function (toSVG) {
return function () {
const svgString = toSVG.call(this);
const domParser = new DOMParser();
const doc = domParser.parseFromString(svgString, "image/svg+xml");
let type = this.type;
if (this.type == "group") {
const objects = this.getObjects();
type = objects[0].type;
const parentG = doc.querySelector(`${type}`);
parentG.setAttribute("name", objects[0].name);
} else {
const parentG = doc.querySelector(`${type}`);
parentG.setAttribute("name", this.name);
}
return doc.documentElement.outerHTML;
};
})(obj.toSVG);
}
//删除选中的对象
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上重绘预览,导出svg格式
function getAllShape() {
canvas.discardActiveObject();
const data = [];
const all_obj = canvas.getObjects(); //获取canvas上所有对象
const prop = {
selectable: false,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingFlip: true,
lockSkewingX: true,
lockSkewingY: true,
lockScalingX: true,
lockScalingY: true,
hasControls: false,
};
const data_json = canvas.toJSON(["name"]); //转换时包含自定义属性Id
canvas_re.loadFromJSON(data_json, canvas_re.renderAll.bind(canvas_re), function (o, obj) {
obj.set(prop);
obj.set({ fill: getCorlor(obj.get("name")) });
});
console.log(canvas_re.getObjects()); //结果为空,有点奇怪
document.querySelector("#info").innerHTML = data_json["objects"].length;
const svgString = canvas.toSVG();
$("#genSvg").html(svgString);
// 下载 SVG 文件
const file = new Blob([svgString], { type: "image/svg+xml" });
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = "test.svg";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
//根据自定义属性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) {
if (opt.target && opt.target.type !== "activeSelection" && opt.target["name"] != "auxiliary_line_h" && opt.target["name"] != "auxiliary_line_v") {
let name;
if (opt.target.type == "group") {
const objects = opt.target.getObjects();
name = objects[0].get("name");
} else {
name = opt.target.get("name");
}
$("#tooltip div span:last-child").text(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) {
if (opt.target && opt.target.type !== "activeSelection") {
let name;
if (opt.target.type == "group") {
const objects = opt.target.getObjects();
name = objects[0].get("name");
} else {
name = opt.target.get("name");
}
$("#tooltip div span:last-child").text(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") {
if (active_obj.type == "group") {
const objects = active_obj.getObjects();
$("#object_name").val(objects[0].get("name"));
} else {
$("#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") {
if (active_obj.type == "group") {
const objects = active_obj.getObjects();
$("#object_name").val(objects[0].get("name"));
} else {
$("#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 });
break;
case 38: // 上键
active_obj.set({ top: active_obj.get("top") - 1 });
break;
case 39: // 右键
active_obj.set({ left: active_obj.get("left") + 1 });
break;
case 40: // 下键
active_obj.set({ top: active_obj.get("top") + 1 });
break;
default:
break;
}
}
}
canvas.renderAll();
});
</script>
</body>
下图中图1为canvas画布,图2为json导出的json重画,图3为导出svg字符串放到html中
绿线为fabric提供的辅助线组件,移动图形对齐时显示,非常好用。不方便截图
实现矩形区域比较简单,实现环形区域过程中比较费劲,最终采用了2个不同直径的同心圆形,记录下踩的坑。
<script src="../plugins/fabric.min.js"></script>
<div>
<canvas id="example" style="border: solid 1px #ccc"></canvas>
<div id="genSvg"></div>
</div>
<script>
const canvas = new fabric.Canvas("example");
canvas.setWidth(600);
canvas.setHeight(100);
{
const circle = new fabric.Circle({
top: 0,
left: 0,
radius: 30,
fill: "red",
});
canvas.add(circle);
}
{
const circle = new fabric.Circle({
top: 0,
left: 60,
radius: 30,
fill: "red",
});
const clipPath = new fabric.Circle({
radius: 10,
top: -10,
left: -10,
});
circle.clipPath = clipPath;
canvas.add(circle);
}
{
const circle = new fabric.Circle({
top: 0,
left: 120,
radius: 30,
fill: "red",
});
const clipPath = new fabric.Circle({
radius: 10,
top: -10,
left: -10,
});
clipPath.inverted = true;
circle.clipPath = clipPath;
canvas.add(circle);
}
{
const x = 210;
const y = 30;
const r1 = 30;
const r2 = 10;
const point1_x = r1 + x;
const point2_x = point1_x - 2 * r1;
const point3_x = r2 + x;
const point4_x = point3_x - 2 * r2;
const path = new fabric.Path(
`M${point1_x},${y} A${r1},${r1} 0 0,1 ${point2_x},${y}
A${r1},${r1} 0 0,1 ${point1_x},${y}
M${point3_x},${y} A${r2},${r2} 0 0,1 ${point4_x},${y}
A${r2},${r2} 0 0,1 ${point3_x},${y}`,
{
stroke: "red",
fill: "transparent",
hasControls: false,
}
);
canvas.add(path);
}
{
const x = 270;
const y = 30;
const r1 = 30;
const r2 = 10;
const point1_x = r1 + x;
const point2_x = point1_x - 2 * r1;
const point3_x = r2 + x;
const point4_x = point3_x - 2 * r2;
const path = new fabric.Path(
`M${point1_x},${y} A${r1},${r1} 0 0,1 ${point2_x},${y}
A${r1},${r1} 0 0,1 ${point1_x},${y}
M${point3_x},${y} A${r2},${r2} 0 0,1 ${point4_x},${y}
A${r2},${r2} 0 0,1 ${point3_x},${y}`,
{
fill: "red",
hasControls: false,
}
);
canvas.add(path);
}
{
const circle = new fabric.Circle({
top: 0,
left: 300,
radius: 25,
stroke: "red",
strokeWidth: 10,
fill: "transparent",
});
canvas.add(circle);
}
document.querySelector("#genSvg").innerHTML = canvas.toSVG();
</script>
上面一行为canvas画布,下面一行为导出的svg字符串放到html中,圆1为填充圆形,圆2、3为采用clipPath实现,圆4、5为采用path实现,,圆4、5为采用path实现stroke实现
1 使用 clipPath实现,导出的svg图和显示不一样
2 使用path实现:导出svg图片和显示一样,缺点:fill属性设置后即为填充圆形,而不是填充圆环
3 使用stroke实现 :圆半径为内径和外径的均值,strokeWidth为外径和内径的差值,导出svg图片和显示一样,然echarts的map似乎不支持stroke
以上3中实现圆环区域的方法利用导出json重画均无问题,只是生成svg图存在各种坑。
标签:canvas,const,fabric,svg,js,vals,active,obj,name From: https://www.cnblogs.com/caroline2016/p/18101701