konva-label
Version:
基于konvajs实现的图片标注工具,提供简单易用的API接口,支持图片加载、缩放、标注框绘制和编辑等功能。
917 lines (901 loc) • 40.9 kB
JavaScript
(function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document);
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('konva')) :
typeof define === 'function' && define.amd ? define(['konva'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.KonvaLabel = factory(global.Konva));
})(this, (function (Konva) { 'use strict';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
// 获取相对于缩放的坐标
var getRelativePointerPosition = function (node) {
var transform = node.getAbsoluteTransform().copy();
transform.invert();
var pos = node.getStage().getPointerPosition();
return transform.point(pos);
};
function getStageAbsoluteDimensions(stage) {
var _a = __read([
stage.width() * stage.scaleX(),
stage.height() * stage.scaleY(),
], 2), scaledStageWidth = _a[0], scaledStageHeight = _a[1];
var _b = __read([stage.x(), stage.y()], 2), stageX = _b[0], stageY = _b[1];
return {
width: scaledStageWidth,
height: scaledStageHeight,
x: stageX,
y: stageY,
};
}
function fitBBoxToScaledStage(box, stage) {
var x = box.x, y = box.y, width = box.width, height = box.height;
var _a = __read([box.x - stage.x, box.y - stage.y], 2), realX = _a[0], realY = _a[1];
if (realX < 0) {
x = stage.x;
width += realX;
}
else if (realX + box.width > stage.width) {
width = stage.width - realX;
}
if (realY < 0) {
y = stage.y;
height += realY;
}
else if (realY + box.height > stage.height) {
height = stage.height - realY;
}
return __assign(__assign({}, box), { x: x, y: y, width: width, height: height });
}
function getBoundingBoxAfterChanges(rect, shiftPoint, radRotation) {
if (radRotation === void 0) { radRotation = 0; }
var transform = new Konva.Transform();
transform.translate(shiftPoint.x, shiftPoint.y);
transform.rotate(radRotation);
return getBoundingBoxAfterTransform(rect, transform);
}
function getBoundingBoxAfterTransform(rect, transform) {
var points = [
{ x: rect.x, y: rect.y },
{ x: rect.x + rect.width, y: rect.y },
{ x: rect.x + rect.width, y: rect.y + rect.height },
{ x: rect.x, y: rect.y + rect.height },
];
var minX;
var minY;
var maxX;
var maxY;
points.forEach(function (point) {
var transformed = transform.point(point);
if (minX === undefined) {
minX = maxX = transformed.x;
minY = maxY = transformed.y;
}
minX = Math.min(minX, transformed.x);
minY = Math.min(minY, transformed.y);
maxX = Math.max(maxX, transformed.x);
maxY = Math.max(maxY, transformed.y);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
/**
* 创建图片缓存
* @param {string} url 图片地址
*/
function imageOnLoad(url) {
return new Promise(function (resolve, reject) {
var img = new Image();
img.src = url;
img.onload = function () {
resolve(img);
};
img.onerror = function (error) {
reject(new Error("\u56FE\u7247\u52A0\u8F7D\u5931\u8D25".concat(url, ": ").concat(JSON.stringify(error))));
};
});
}
// export function convertToRGBA(color: string, opacity: number): string {
// // 创建一个临时 canvas 来解析颜色
// const canvas = document.createElement('canvas');
// const ctx = canvas.getContext('2d')!;
// // 设置颜色
// ctx.fillStyle = color;
// // 获取颜色值
// const [r, g, b] = ctx.fillStyle
// .replace(/^#/, '')
// .match(/.{2}/g)!
// .map((x) => parseInt(x, 16));
// // 确保透明度在 0-1 之间
// const alpha = Math.max(0, Math.min(1, opacity));
// return `rgba(${r}, ${g}, ${b}, ${alpha})`;
// }
var toRgba = function (color, alpha) {
var _a, _b;
if (alpha === void 0) { alpha = 1; }
if (!color)
return color;
var r, g, b;
if (color.startsWith('#')) {
var hex_1 = color.slice(1);
if (hex_1.length === 3) {
hex_1 = hex_1
.split('')
.map(function (x) { return x + x; })
.join('');
}
_a = __read([0, 2, 4].map(function (start) {
return parseInt(hex_1.slice(start, start + 2), 16);
}), 3), r = _a[0], g = _a[1], b = _a[2];
}
else if (color.startsWith('rgb')) {
_b = __read(color.match(/\d+/g).map(Number), 3), r = _b[0], g = _b[1], b = _b[2];
}
else if (color.startsWith('hsl')) {
var _c = __read(color.match(/\d+/g).map(Number), 3), h = _c[0], s = _c[1], l = _c[2];
// 将 HSL 转换为 RGB
var hue = h / 360;
var saturation = s / 100;
var lightness = l / 100;
if (saturation === 0) {
r = g = b = Math.round(lightness * 255);
}
else {
var hue2rgb = function (p, q, t) {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6)
return p + (q - p) * 6 * t;
if (t < 1 / 2)
return q;
if (t < 2 / 3)
return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = lightness < 0.5
? lightness * (1 + saturation)
: lightness + saturation - lightness * saturation;
var p = 2 * lightness - q;
r = Math.round(hue2rgb(p, q, hue + 1 / 3) * 255);
g = Math.round(hue2rgb(p, q, hue) * 255);
b = Math.round(hue2rgb(p, q, hue - 1 / 3) * 255);
}
}
else {
throw new Error('Invalid color format');
}
return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(Math.max(0, Math.min(1, alpha)), ")");
};
//js防抖
function debounce(fn, delay) {
var timer = null;
return function () {
var _this = this;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (timer)
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(_this, args);
}, delay);
};
}
var LABEL_CONFIG = {
color: 'rgb(255, 0, 0)',
fillOpacity: 0.2,
selectOpacity: 0.5,
fontSize: 16,
strokeWidth: 1,
textGap: 6,
};
var KonvaLabel = /** @class */ (function () {
function KonvaLabel(_a) {
var el = _a.el, onChange = _a.onChange, labelConfig = _a.labelConfig;
var _this = this;
this.image = null; //图片的原始信息
this.zoomRatio = 1; //图片的缩放比例
this.imageNode = null; //图片在画画布上的实列
this.bboxes = []; //标注框
this.labelConfig = LABEL_CONFIG; //标注框的配置
this.currentShape = null; //当前绘制的标注框
this.labelInfo = {}; //新绘制标签的名称
this.transformer = null; //标注框变形
this.onChange = null;
/**
* 限制画布活动范围不超过图片
*/
this.dragmove = function () {
if (!_this.imageNode)
return;
var imageWidth = _this.imageNode.width();
var imageHeight = _this.imageNode.height();
var stagePos = _this.stage.position();
var stageSize = _this.stage.size();
var scale = _this.stage.scaleX(); // 假设 x 和 y 的缩放比例相同
// 计算放大后的画布大小
var scaledWidth = stageSize.width * scale;
var scaledHeight = stageSize.height * scale;
// 确保画布的左边和上边不超过图片的左边和上边
var newX = Math.min(0, Math.max(stagePos.x, imageWidth - scaledWidth));
var newY = Math.min(0, Math.max(stagePos.y, imageHeight - scaledHeight));
// 更新舞台位置
_this.stage.position({ x: newX, y: newY });
_this.stage.batchDraw();
};
/**
* 鼠标滚轮事件,控制舞台缩放
*/
this.wheel = function (e) {
if (_this.currentShape)
return;
e.evt.preventDefault(); //阻止默认滚轮行为
var oldScale = _this.stage.scaleX(); //获取舞台当前的缩放比例
var pointer = _this.stage.getPointerPosition(); //获取鼠标指针的位置
//计算鼠标指针在舞台中的位置
var mousePointTo = {
x: (pointer.x - _this.stage.x()) / oldScale,
y: (pointer.y - _this.stage.y()) / oldScale,
};
var isZoomingOut = e.evt.deltaY > 0; //获取鼠标滚轮的方向
// 限制最大和最小缩放比例
var MAX_SCALE = 5;
var MIN_SCALE = 1;
var newScale = isZoomingOut
? Math.max(oldScale * 0.9, MIN_SCALE)
: Math.min(oldScale * 1.1, MAX_SCALE); //计算新的缩放比例
_this.stage.scale({ x: newScale, y: newScale }); //缩放舞台
// 计算缩放后的舞台尺寸
var scaledWidth = _this.stage.width() * newScale;
var scaledHeight = _this.stage.height() * newScale;
// 计算新的位置,确保舞台不会超出视野
var newPos = {
x: Math.max(Math.min(0, pointer.x - mousePointTo.x * newScale), _this.stage.width() - scaledWidth),
y: Math.max(Math.min(0, pointer.y - mousePointTo.y * newScale), _this.stage.height() - scaledHeight),
};
_this.stage.position(newPos);
_this.stage.batchDraw();
};
// 加载图片
// 调整舞台大小
this.fitStageToContainer = debounce(function () {
var zoomRatio = _this.getZoomRatio();
var containerHeight = _this.image.height * zoomRatio;
var scaledWidth = _this.image.width * zoomRatio;
_this.stage.width(scaledWidth);
_this.stage.height(containerHeight);
if (_this.imageNode) {
// 设置舞台宽度为新的图片宽度
_this.imageNode.size({
width: scaledWidth,
height: containerHeight,
});
// 不需要居中,x坐标始终为0
_this.imageNode.x(0);
_this.imageNode.y(0);
}
_this.updateLable();
_this.layer.draw();
}, 200);
/**
* 鼠标左键按下事件(开始绘制)
*/
this.mousedown = function () {
if (_this.stage.draggable())
return;
if (_this.currentShape)
return _this.mouseup();
var pos = getRelativePointerPosition(_this.layer);
_this.currentShape = _this.createLabel({
id: Date.now().toString(),
x: pos.x,
y: pos.y,
width: 0,
height: 0,
});
};
/**
* 鼠标移动事件(绘制中)
*/
this.mousemove = function () {
if (!_this.currentShape)
return;
var pos = getRelativePointerPosition(_this.layer);
// 计算新的宽度和高度
var width = pos.x - _this.currentShape.x();
var height = pos.y - _this.currentShape.y();
// 确保矩形不会超出画布
var stageSize = _this.stage.size();
if (width > 0) {
// 向右绘制时,确保右边界不超出画布
var maxRightX = stageSize.width - _this.currentShape.x();
width = Math.min(width, maxRightX);
}
else {
// 向左绘制时,确保不超出左边界
width = Math.max(width, -_this.currentShape.x());
}
if (height > 0) {
// 向下绘制时,确保下边界不超出画布
var maxBottomY = stageSize.height - _this.currentShape.y();
height = Math.min(height, maxBottomY);
}
else {
// 向上绘制时,确保不超出上边界
height = Math.max(height, -_this.currentShape.y());
}
_this.currentShape.height(height);
_this.currentShape.width(width);
_this.layer.batchDraw();
};
/**
* 鼠标左键抬起(绘制结束)
*/
this.mouseup = function () {
if (!_this.currentShape)
return;
// 获取舞台大小
var stageSize = _this.stage.size();
// 计算最终的位置和大小
var finalX = _this.currentShape.x();
var finalY = _this.currentShape.y();
var width = _this.currentShape.width();
var height = _this.currentShape.height();
// 处理负宽度/高度的情况
if (width < 0) {
finalX += width;
width = Math.abs(width);
}
if (height < 0) {
finalY += height;
height = Math.abs(height);
}
// 确保标注框完全在画布内
if (finalX + width > stageSize.width) {
width = stageSize.width - finalX;
}
if (finalY + height > stageSize.height) {
height = stageSize.height - finalY;
}
// 如果标注框太小,就删除它
if (width < 5 || height < 5) {
_this.currentShape.destroy();
}
else {
// 设置最终的位置和大小
_this.currentShape.position({
x: Math.max(0, finalX),
y: Math.max(0, finalY),
});
_this.currentShape.size({
width: width,
height: height,
});
// 为新创建的矩形添加拖动约束
_this.updateText(_this.currentShape);
_this.emitChange('add'); // 触发添加事件
}
_this.cancelDraw();
_this.layer.draw();
};
/**
* 限制标注框的变换范围不超过图片
*/
this.constrainSizes = function (oldBox, newBox) {
if (!_this.transformer)
return;
// it's important to compare against `undefined` because it can be missed (not rotated box?)
var rotation = newBox.rotation !== undefined ? newBox.rotation : oldBox.rotation;
var isRotated = rotation !== oldBox.rotation;
var stageDimensions = getStageAbsoluteDimensions(_this.stage);
if (newBox.width < 3)
newBox.width = 3;
if (newBox.height < 3)
newBox.height = 3;
// // it's harder to fix sizes for rotated box, so just block changes out of stage
if (rotation || isRotated) {
var x = newBox.x, y = newBox.y, width = newBox.width, height = newBox.height;
var selfRect = { x: 0, y: 0, width: width, height: height };
// bounding box, got by applying current shift and rotation to normalized box
var clientRect_1 = getBoundingBoxAfterChanges(selfRect, { x: x, y: y }, rotation);
var fixed_1 = fitBBoxToScaledStage(clientRect_1, stageDimensions);
// if bounding box is out of stage — do nothing
if (['x', 'y', 'width', 'height'].some(function (key) { return Math.abs(fixed_1[key] - clientRect_1[key]) > 0.001; }))
return oldBox;
return newBox;
}
_this.updateText(_this.transformer.nodes()[0]);
return fitBBoxToScaledStage(newBox, stageDimensions);
};
/**
* 更新标注框标签的位置
* @param {Konva.Rect} rect konva.Rect的实列
*/
this.updateText = debounce(function (rect) {
var textList = _this.getAllText();
var find = textList.find(function (i) { return i.id() === rect.id(); });
find === null || find === void 0 ? void 0 : find.setAttrs({
x: rect.x(),
y: rect.y() - _this.labelConfig.textGap - _this.labelConfig.fontSize,
text: rect.attrs.data.label,
});
}, 200);
this.stageContainer = el;
var container = this.getStageContainerNode();
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.alignItems = 'center';
this.stage = new Konva.Stage({
container: container,
width: container.clientWidth,
height: container.clientHeight,
draggable: true,
});
this.layer = new Konva.Layer();
this.stage.add(this.layer);
if (onChange)
this.onChange = onChange;
if (labelConfig)
this.labelConfig = Object.assign(this.labelConfig, labelConfig);
this.bindEvents();
}
//绑定事件
KonvaLabel.prototype.bindEvents = function () {
var _this = this;
//mousedown mouseup moveuchmove 三个事件用来实现绘制标注框的功能
this.stage.on('mousedown', this.mousedown);
this.stage.on('mousemove', this.mousemove);
this.stage.on('mouseup', this.mouseup);
this.stage.on('dragmove', this.dragmove); //舞台拖拽
this.stage.on('wheel', this.wheel); //舞台缩放事件
//画布点击事件
this.stage.on('click tap', function (e) {
_this.createTransformer(e.target); // 只有在移动工具下点击选中标记框
});
window.addEventListener('resize', this.fitStageToContainer);
};
// 获取舞台的节点
KonvaLabel.prototype.getStageContainerNode = function () {
return document.getElementById(this.stageContainer);
};
/**
* 清除layer图层中的所有元素,并还原舞台的缩放和位置
*/
KonvaLabel.prototype.destroyChildren = function () {
if (!this.layer)
return;
this.layer.destroyChildren(); //清除layer图层标注框
this.image = null;
this.stage.scale({ x: 1, y: 1 });
this.stage.position({ x: 0, y: 0 });
};
KonvaLabel.prototype.createLabel = function (_a) {
var _b = _a.id, id = _b === void 0 ? Date.now().toString() : _b, x = _a.x, y = _a.y, width = _a.width, height = _a.height, _c = _a.labelInfo, labelInfo = _c === void 0 ? this.labelInfo : _c;
var fill = toRgba((labelInfo.color || this.labelConfig.color), this.labelConfig.fillOpacity);
var stroke = labelInfo.color || this.labelConfig.color;
var rect = new Konva.Rect({
id: id,
x: x,
y: y,
width: width,
height: height,
name: 'rect',
fill: fill,
stroke: stroke,
strokeWidth: 1,
draggable: false,
strokeScaleEnabled: false, //防止transformer缩放时,矩形边框被拉伸
data: __assign(__assign({}, labelInfo), { id: id }),
});
var text = new Konva.Text({
id: id,
x: x,
y: y - this.labelConfig.textGap - this.labelConfig.fontSize,
fontSize: this.labelConfig.fontSize,
text: labelInfo.label || '',
fill: stroke,
});
this.layer.add(text);
this.layer.add(rect);
this.dragBoundFunc(rect);
return rect;
};
/**
* 更新所有标注框和标签的尺寸和位置(窗口尺寸发生变化时调用)
*/
KonvaLabel.prototype.updateLable = function () {
var _this = this;
if (!this.image || !this.bboxes.length)
return;
var rectangles = this.getAllRect();
// 更新所有文本标签
var texts = this.getAllText();
this.bboxes.forEach(function (item, index) {
var _a, _b, _c, _d, _e, _f;
(_a = rectangles[index]) === null || _a === void 0 ? void 0 : _a.x(item.box[0] * _this.zoomRatio);
(_b = rectangles[index]) === null || _b === void 0 ? void 0 : _b.y(item.box[1] * _this.zoomRatio);
(_c = rectangles[index]) === null || _c === void 0 ? void 0 : _c.width((item.box[2] - item.box[0]) * _this.zoomRatio);
(_d = rectangles[index]) === null || _d === void 0 ? void 0 : _d.height((item.box[3] - item.box[1]) * _this.zoomRatio);
(_e = texts[index]) === null || _e === void 0 ? void 0 : _e.x(item.box[0] * _this.zoomRatio);
(_f = texts[index]) === null || _f === void 0 ? void 0 : _f.y(item.box[1] * _this.zoomRatio -
_this.labelConfig.textGap -
_this.labelConfig.fontSize);
});
};
/**
* 限制选中标注框的活动范围不超过图片
* @param {Konva.Rect} rect konva.Rect实列
*/
KonvaLabel.prototype.dragBoundFunc = function (rect) {
var _this = this;
rect.dragBoundFunc(function (pos) {
var stageSize = _this.stage.size();
var stageScale = _this.stage.scaleX();
// 计算出矩形实际的宽高
var rectWidth = rect.width() * rect.scaleX() * stageScale;
var rectHeight = rect.height() * rect.scaleY() * stageScale;
// 计算舞台的边界考虑了缩放
var stageLeft = _this.stage.x();
var stageTop = _this.stage.y();
var stageRight = stageLeft + stageSize.width * stageScale;
var stageBottom = stageTop + stageSize.height * stageScale;
// 调整矩形的位置以防止超出舞台边界
var newX = Math.max(stageLeft, Math.min(pos.x, stageRight - rectWidth));
var newY = Math.max(stageTop, Math.min(pos.y, stageBottom - rectHeight));
_this.updateText(rect);
return {
x: newX,
y: newY,
};
});
};
KonvaLabel.prototype.emitChange = function (type) {
var _this = this;
var rects = this.getAllRect();
var textList = this.getAllText();
this.bboxes = rects.map(function (item, index) {
var obj = __assign({}, item.attrs.data);
var x = item.x() / _this.zoomRatio;
var y = item.y() / _this.zoomRatio;
var width = (item.width() * item.scaleX()) / _this.zoomRatio;
var height = (item.height() * item.scaleY()) / _this.zoomRatio;
var box = [Math.round(x), Math.round(y), Math.round(x + width), Math.round(y + height)];
obj.box = box;
obj.rect = rects[index];
obj.text = textList[index];
return obj;
});
this.onChange &&
this.onChange({
type: type,
data: this.bboxes,
});
};
/**
* # 获取缩放比
*/
KonvaLabel.prototype.getZoomRatio = function () {
var container = this.getStageContainerNode();
var containerRatio = container.clientWidth / container.clientHeight;
var imageRatio = this.image.width / this.image.height;
var ratio = 1;
if (imageRatio > containerRatio) {
ratio = container.clientWidth / this.image.width;
}
else {
ratio = container.clientHeight / this.image.height; // 以高为基准
}
this.zoomRatio = ratio;
return ratio;
};
/**
* # 加载图片
*/
KonvaLabel.prototype.loadImage = function (img_1) {
return __awaiter(this, arguments, void 0, function (img, bboxes) {
var _a;
var _this = this;
if (bboxes === void 0) { bboxes = []; }
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
this.cancelDraw();
this.destroyChildren();
if (!(img instanceof HTMLImageElement)) return [3 /*break*/, 1];
this.image = img;
return [3 /*break*/, 3];
case 1:
_a = this;
return [4 /*yield*/, imageOnLoad(img)];
case 2:
_a.image = _b.sent();
_b.label = 3;
case 3:
this.imageNode = new Konva.Image({
image: this.image,
width: 0,
height: 0,
x: 0, // 不需要居中,因为画布宽度等于图片宽度
y: 0,
});
this.layer.add(this.imageNode);
this.imageNode.moveToBottom();
this.fitStageToContainer();
this.layer.draw();
//绘制标注框,初始化的
if (bboxes.length) {
this.bboxes = bboxes;
bboxes.forEach(function (item) {
_this.drawBox(item);
});
}
this.emitChange('init');
return [2 /*return*/, 'ok'];
}
});
});
};
/**
* 获取所有文本
*/
KonvaLabel.prototype.getAllText = function () {
return this.layer.getChildren(function (node) { return node.getClassName() === 'Text'; });
};
/**
* 获取所有标注框
*/
KonvaLabel.prototype.getAllRect = function () {
return this.layer.getChildren(function (node) { return node.getClassName() === 'Rect'; });
};
//获取选中的节点
KonvaLabel.prototype.getTransformerNodes = function () {
if (!this.transformer)
return [];
return this.transformer.nodes();
};
//取消绘制标注框
KonvaLabel.prototype.cancelDraw = function () {
this.labelInfo = {}; //重置新绘制标签的名称
this.stage.draggable(true); //设置画布拖动
this.stage.container().style.cursor = 'default';
this.currentShape = null;
this.transformer && this.transformer.nodes([]);
};
KonvaLabel.prototype.draw = function (labelInfo) {
this.labelInfo = labelInfo;
this.stage.draggable(false);
this.stage.container().style.cursor = 'crosshair';
};
/**
* 删除选中的标注
*/
KonvaLabel.prototype.deleteSelected = function () {
var _this = this;
if (!this.transformer)
return;
var selectedNodes = this.transformer.nodes();
var textList = this.getAllText();
selectedNodes.forEach(function (node) {
var find = textList.find(function (i) { return i.id() === node.id(); });
if (find) {
find.destroy();
node.destroy();
_this.emitChange('delete');
}
});
this.transformer.nodes([]);
this.layer.draw();
};
/**
* 通过id删除标注
* @param {string} id 标注id
*/
KonvaLabel.prototype.deleteById = function (id) {
var textList = this.getAllText();
var rectList = this.getAllRect();
var findIndex = rectList.findIndex(function (i) { return i.id() === id; });
if (findIndex !== -1) {
var find = rectList[findIndex];
var findText = textList[findIndex];
findText === null || findText === void 0 ? void 0 : findText.destroy();
find === null || find === void 0 ? void 0 : find.destroy();
this.emitChange('delete');
}
};
/**
* 选中标注框进行编辑
* @param {Konva.Rect} rect konva.Rect的实列
*/
KonvaLabel.prototype.createTransformer = function (rect) {
var _this = this;
if (this.transformer) {
//取消选择
this.transformer.nodes().forEach(function (i) {
i.draggable(false);
//回复原来的样式
i.setAttr('fill', toRgba(i.getAttr('stroke'), _this.labelConfig.fillOpacity)); //回复原来的颜色
});
this.transformer.destroy(); //销毁
this.transformer = null; //清空
this.emitChange('update'); //更新
}
else {
//选择选中的标注框
if (rect && rect.getClassName() === 'Rect') {
rect.draggable(true);
this.transformer = new Konva.Transformer({
rotateEnabled: true,
flipEnabled: false,
ignoreStroke: true,
keepRatio: false,
resizeEnabled: true,
anchorSize: 8,
zoomedIn: this.stage.scaleX() > 1,
boundBoxFunc: this.constrainSizes,
});
rect.setAttr('fill', toRgba(rect.getAttr('stroke'), this.labelConfig.selectOpacity)); //选中时的颜色
this.layer.add(this.transformer);
this.transformer.attachTo(rect);
this.layer.draw();
}
}
};
/**
* 修改选中透明度
* @param {number} opacity 透明度
*/
KonvaLabel.prototype.updateSelectOpacity = function (number) {
var _this = this;
this.labelConfig.selectOpacity = number;
var transformerNodes = this.getTransformerNodes();
transformerNodes.length &&
transformerNodes.forEach(function (item) {
item.setAttr('fill', toRgba(item.getAttr('stroke'), _this.labelConfig.selectOpacity));
});
};
/**
* 更新填充透明度
*/
KonvaLabel.prototype.updateFillOpacity = function (number) {
var _this = this;
this.labelConfig.fillOpacity = number;
var rects = this.getAllRect();
var transformerNodes = this.getTransformerNodes();
rects.forEach(function (item) {
if (!transformerNodes.length ||
transformerNodes.every(function (node) { return node.id() !== item.id(); })) {
item.setAttr('fill', toRgba(item.getAttr('stroke'), _this.labelConfig.fillOpacity));
}
});
};
/**
* 更新标注框的名称
*/
KonvaLabel.prototype.updateLabelName = function (id, data) {
var rectList = this.getAllRect();
var find = rectList.find(function (i) { return i.id() == id; });
if (find) {
find.attrs.data = data;
this.updateText(find);
this.emitChange('update');
}
};
/**
* 重置缩放工具
*/
KonvaLabel.prototype.resetZoom = function () {
this.stage.scale({ x: 1, y: 1 });
this.stage.position({ x: 0, y: 0 });
this.layer.draw();
};
/**
* # 绘制已经存在的标注框
* @param {Array} data 标注框的原始数据
*/
KonvaLabel.prototype.drawBox = function (data) {
var left = data.box[0] * this.zoomRatio;
var top = data.box[1] * this.zoomRatio;
var width = (data.box[2] - data.box[0]) * this.zoomRatio;
var height = (data.box[3] - data.box[1]) * this.zoomRatio;
this.createLabel({
id: data.id || Date.now().toString(),
x: left,
y: top,
width: width,
height: height,
labelInfo: __assign(__assign({}, data), { label: data.label }),
});
};
KonvaLabel.prototype.destroy = function () {
if (this.stage) {
this.stage.destroy();
}
window.removeEventListener('resize', this.fitStageToContainer);
};
return KonvaLabel;
}());
return KonvaLabel;
}));
//# sourceMappingURL=konva-label.min.js.map