UNPKG

canvas-select-vite

Version:

一个用于图片标注的 javascript 库,基于 canvas,简单轻量,支持矩形、多边形、点、折线、圆形标注、网格标注。

1,064 lines (1,063 loc) 45.1 kB
var P = Object.defineProperty; var N = (v, g, t) => g in v ? P(v, g, { enumerable: !0, configurable: !0, writable: !0, value: t }) : v[g] = t; var h = (v, g, t) => N(v, typeof g != "symbol" ? g + "" : g, t); function O() { const v = [], g = "0123456789abcdef"; for (let e = 0; e < 36; e++) { const i = Math.floor(Math.random() * 16); v[e] = g.slice(i, i + 1); } v[14] = "4"; const t = v[19] & 3 | 8; return v[19] = g.slice(t, t + 1), v[8] = v[13] = v[18] = v[23] = "-", v.join(""); } function X(v, g) { if (v.type === 1 && g.type === 1) { const [[t, e], [i, s]] = v.coor, [[a, o], [c, r]] = g.coor; return t <= a && e <= o && i >= c && s >= r; } else if (v.type === 1 && g.type === 2) { const [[t, e], [i, s]] = v.coor, a = g.coor; for (let o = 0; o < a.length; o++) { const [c, r] = a[o]; if (c < t || c > i || r < e || r > s) return !1; } return !0; } else if (v.type === 2 && g.type === 1) { const t = g.coor; for (let e = 0; e < t.length; e++) { const [i, s] = t[e]; if (!w(i, s, v.coor)) return !1; } return !0; } else if (v.type === 2 && g.type === 2) { const t = v.coor, e = g.coor; for (let i = 0; i < e.length; i++) { const [s, a] = e[i]; if (!w(s, a, t)) return !1; } return !0; } return !1; } function w(v, g, t) { let e = !1; const i = t.length; for (let s = 0, a = i - 1; s < i; a = s++) { const o = t[s][0], c = t[s][1], r = t[a][0], l = t[a][1]; c > g != l > g && v < (r - o) * (g - c) / (l - c) + o && (e = !e); } return e; } class E { constructor(g, t) { /** 标签 */ h(this, "label", ""); /** 是否隐藏标签 */ //@ts-ignore h(this, "hideLabel"); /** 坐标 */ h(this, "coor", []); /** 边线颜色 */ //@ts-ignore h(this, "strokeStyle"); /** 填充颜色 */ //@ts-ignore h(this, "fillStyle"); /** 边线宽度 */ //@ts-ignore h(this, "lineWidth"); /** 标签填充颜色 */ //@ts-ignore h(this, "labelFillStyle"); /** 标签文字颜色 */ //@ts-ignore h(this, "textFillStyle"); /** 标签文字字体 */ //@ts-ignore h(this, "labelFont"); /** 1 矩形,2 多边形,3 点,4 折线,5 圆,6 网格 */ //@ts-ignore h(this, "type"); // 形状 /** 当前是否处于活动状态 */ h(this, "active", !1); /** 当前是否处于创建状态 */ h(this, "creating", !1); /** 当前是否处于拖拽状态 */ h(this, "dragging", !1); /** 索引 */ h(this, "index"); /** 唯一标识 */ h(this, "uuid", O()); /** 向上展示label */ //@ts-ignore h(this, "labelUp"); /** 隐藏标注 */ //@ts-ignore h(this, "hide"); this.index = t, Object.assign(this, g); } } class T extends E { constructor(t, e) { super(t, e); h(this, "type", 1); } get ctrlsData() { const [[t, e], [i, s]] = this.coor; return [ [t, e], [t + (i - t) / 2, e], [i, e], [i, e + (s - e) / 2], [i, s], [t + (i - t) / 2, s], [t, s], [t, e + (s - e) / 2] ]; } } class C extends E { constructor(t, e) { super(t, e); h(this, "type", 2); } get ctrlsData() { return this.coor.length > 2 ? this.coor : []; } } class D extends E { constructor(t, e) { super(t, e); h(this, "type", 3); } } class Y { constructor() { h(this, "_eventTree", {}); } /** * 注册事件 * @param eventName 事件名称 * @param cb 回调方法 */ on(g, t) { const e = this._eventTree[g]; Array.isArray(e) ? e.push(t) : this._eventTree[g] = [t]; } /** * 触发事件 * @param eventName 事件名称 * @param payload 传递参数 */ emit(g, ...t) { const e = this._eventTree[g]; Array.isArray(e) && e.forEach((i) => i(...t)); } /** * 注销事件 * @param eventName 事件名称 * @param cb 传递参数 */ off(g, t) { const e = this._eventTree[g], i = e.find((s) => s === t); Array.isArray(e) && i && e.splice(i, 1); } } class k extends E { constructor(t, e) { super(t, e); h(this, "type", 4); } get ctrlsData() { return this.coor.length > 1 ? this.coor : []; } } class _ extends E { constructor(t, e) { super(t, e); h(this, "type", 5); h(this, "radius", 0); this.radius = t.radius || this.radius; } get ctrlsData() { const [t, e] = this.coor; return [ [t, e - this.radius], [t + this.radius, e], [t, e + this.radius], [t - this.radius, e] ]; } } class W extends E { constructor(t, e) { super(t, e); h(this, "type", 6); h(this, "row", 1); h(this, "col", 1); h(this, "selected", []); h(this, "selectedFillStyle"); this.row = t.row > 0 ? t.row : this.row, this.col = t.col > 0 ? t.col : this.col, this.selected = Array.isArray(t.selected) ? t.selected : []; } get ctrlsData() { const [[t, e], [i, s]] = this.coor; return [ [t, e], [t + (i - t) / 2, e], [i, e], [i, e + (s - e) / 2], [i, s], [t + (i - t) / 2, s], [t, s], [t, e + (s - e) / 2] ]; } get gridRects() { const [[t, e], [i, s]] = this.coor, { row: a, col: o, strokeStyle: c, fillStyle: r, active: l, creating: I, lineWidth: p } = this, n = (i - t) / this.col, d = (s - e) / this.row, u = []; for (let f = 0; f < a; f++) for (let y = 0; y < o; y++) { const S = [t + y * n, e + f * d], x = f * o + y, m = new T({ coor: [S, [S[0] + n, S[1] + d]], strokeStyle: c, fillStyle: r, active: l, creating: I, lineWidth: p }, x); u.push(m); } return u; } } const F = "canvas-select-vite", z = "0.0.1", U = "module", j = ["dist"], K = "./dist/canvas-select-vite.umd.js", $ = "./dist/canvas-select-vite.es.js", q = "./dist/src/index.d.ts", B = { types: "./dist/src/index.d.ts", import: "./dist/canvas-select-vite.es.js", require: "./dist/canvas-select-vite.umd.js" }, V = { dev: "vite", build: "tsc && vite build" }, Z = { typescript: "~5.7.2", vite: "^6.0.1" }, J = { "vite-plugin-dts": "^4.3.0" }, Q = ["canvas", "marker", "annotation", "rect", "polygon", "dot", "line", "circle", "grid", "标注", "矩形", "多边形", "点", "折线", "圆", "网格"], tt = "heylight", et = "MIT", it = "https://github.com/heylight/canvas-select", st = { type: "git", url: "git+https://github.com/heylight/canvas-select.git" }, ht = { name: F, version: z, type: U, files: j, main: K, module: $, types: q, exports: B, scripts: V, devDependencies: Z, dependencies: J, keywords: Q, author: tt, license: et, homepage: it, repository: st }; class nt extends Y { /** * @param el Valid CSS selector string, or DOM * @param src image src */ constructor(t, e) { super(); /** 当前版本 */ h(this, "version", ht.version); /** 只读模式,画布不允许任何交互 */ h(this, "lock", !1); /** 只读模式,仅支持查看 */ h(this, "readonly", !1); /** 最小矩形宽度 */ h(this, "MIN_WIDTH", 10); /** 最小矩形高度 */ h(this, "MIN_HEIGHT", 10); /** 最小圆形半径 */ h(this, "MIN_RADIUS", 5); /** 边线颜色 */ h(this, "strokeStyle", "#0f0"); /** 填充颜色 */ h(this, "fillStyle", "rgba(0, 0, 255, 0.1)"); /** 边线宽度 */ h(this, "lineWidth", 1); /** 当前选中的标注边线颜色 */ h(this, "activeStrokeStyle", "#f00"); /** 当前选中的标注填充颜色 */ h(this, "activeFillStyle", "rgba(255, 0, 0, 0.1)"); /** 控制点边线颜色 */ h(this, "ctrlStrokeStyle", "#000"); /** 控制点填充颜色 */ h(this, "ctrlFillStyle", "#fff"); /** 控制点半径 */ h(this, "ctrlRadius", 3); /** 是否隐藏标签 */ h(this, "hideLabel", !1); /** 标签背景填充颜色 */ h(this, "labelFillStyle", "#fff"); /** 标签字体 */ h(this, "labelFont", "10px sans-serif"); /** 标签文字颜色 */ h(this, "textFillStyle", "#000"); /** 标签字符最大长度,超出使用省略号 */ h(this, "labelMaxLen", 10); /** 画布宽度 */ h(this, "WIDTH", 0); /** 画布高度 */ h(this, "HEIGHT", 0); h(this, "canvas"); h(this, "ctx"); /** 所有标注数据 */ h(this, "dataset", []); h(this, "offScreen"); h(this, "offScreenCtx"); // 放大镜相关配置 Start h(this, "magnifierCanvas"); h(this, "magnifierCtx"); // 默认展示放大镜 h(this, "isMagnifierVisible", !0); // 放大镜位置,默认跟随鼠标 h(this, "magnifierPosition", "auto"); // 放大镜相关配置 End /** 记录锚点距离 */ h(this, "remmber"); /** 记录鼠标位置 */ h(this, "mouse"); /** 记录背景图鼠标位移 */ h(this, "remmberOrigin", [0, 0]); /** 0 不创建,1 矩形,2 多边形,3 点,4 折线,5 圆,6 网格 */ h(this, "createType", 0); // /** 控制点索引 */ h(this, "ctrlIndex", -1); /** 背景图片 */ h(this, "image", new Image()); /** 图片原始宽度 */ h(this, "IMAGE_ORIGIN_WIDTH"); /** 图片缩放宽度 */ h(this, "IMAGE_WIDTH", 0); /** 图片原始高度 */ h(this, "IMAGE_ORIGIN_HEIGHT", 0); /** 图片缩放高度 */ h(this, "IMAGE_HEIGHT", 0); /** 原点x */ h(this, "originX", 0); /** 原点y */ h(this, "originY", 0); /** 缩放步长 */ h(this, "scaleStep", 0); /** 滚动缩放 */ h(this, "scrollZoom", !0); h(this, "timer"); /** 最小touch双击时间 */ h(this, "dblTouch", 300); /** 记录touch双击开始时间 */ h(this, "dblTouchStore", 0); // /** 这个选项可以帮助浏览器进行内部优化 */ h(this, "alpha", !0); /** 专注模式 */ h(this, "focusMode", !1); /** 记录当前事件 */ h(this, "evt"); /** 触控缩放时记录上一次两点距离 */ h(this, "scaleTouchStore", 0); /** 当前是否为双指触控 */ h(this, "isTouch2", !1); h(this, "isMobile", navigator.userAgent.includes("Mobile")); /** 向上展示label */ h(this, "labelUp", !1); h(this, "isCtrlKey", !1); /** 自定义ctrl快捷键 KeyboardEvent.code */ h(this, "ctrlCode", "ControlLeft"); /** 网格右键菜单 */ h(this, "gridMenuEnable", !0); /** 网格选中背景填充颜色 */ h(this, "gridSelectedFillStyle", "rgba(255, 255, 0, 0.6)"); this.handleLoad = this.handleLoad.bind(this), this.handleContextmenu = this.handleContextmenu.bind(this), this.handleMousewheel = this.handleMousewheel.bind(this), this.handleMouseDown = this.handleMouseDown.bind(this), this.handleMouseMove = this.handleMouseMove.bind(this), this.handleMouseUp = this.handleMouseUp.bind(this), this.handleDblclick = this.handleDblclick.bind(this), this.handleKeyup = this.handleKeyup.bind(this), this.handleKeydown = this.handleKeydown.bind(this); const i = typeof t == "string" ? document.querySelector(t) : t; i instanceof HTMLCanvasElement ? (this.canvas = i, this.offScreen = document.createElement("canvas"), this.initSetting(), this.initEvents(), e && this.setImage(e)) : console.warn("HTMLCanvasElement is required!"); } /** 当前当前选中的标注 */ get activeShape() { return this.dataset.find((t) => t.active) || {}; } /** 当前缩放比例 */ get scale() { return this.IMAGE_ORIGIN_WIDTH && this.IMAGE_WIDTH ? this.IMAGE_WIDTH / this.IMAGE_ORIGIN_WIDTH : 1; } /** 图片最小边尺寸 */ get imageMin() { return Math.min(this.IMAGE_WIDTH, this.IMAGE_HEIGHT); } /** 图片原始最大边尺寸 */ get imageOriginMax() { return Math.max(this.IMAGE_ORIGIN_WIDTH, this.IMAGE_ORIGIN_HEIGHT); } /** 创建放大镜容器 */ createMagnifierCanvas() { this.isMagnifierVisible && (this.magnifierCanvas = this.magnifierCanvas || document.createElement("canvas"), this.magnifierCtx = this.magnifierCanvas && this.magnifierCanvas.getContext("2d", { willReadFrequently: !0 }), this.magnifierCanvas.style.position = "fixed", this.magnifierCanvas.style.pointerEvents = "none", this.magnifierCanvas.style.zIndex = "1000", this.magnifierCanvas.style.border = "1px solid black", this.magnifierCanvas.style.borderRadius = "50%", this.magnifierCanvas.style.width = "100px", this.magnifierCanvas.style.height = "100px", document.body.appendChild(this.magnifierCanvas)); } /** 创建放大镜 */ createMagnifier(t, e) { this.magnifierCanvas ? this.updateMagnifier(t, e) : this.createMagnifierCanvas(); } /** 更新放大镜 */ updateMagnifier(t, e) { if (this.magnifierCanvas && this.magnifierCtx) { const s = window.devicePixelRatio || 1; if (this.magnifierCanvas.width = 100, this.magnifierCanvas.height = 100, this.magnifierCtx.clearRect(0, 0, 100, 100), this.magnifierPosition && this.magnifierPosition.length === 2) { const [r, l] = this.magnifierPosition; this.magnifierCanvas.style.left = `${r}px`, this.magnifierCanvas.style.top = `${l}px`; } else this.magnifierCanvas.style.left = `${t + 10}px`, this.magnifierCanvas.style.top = `${e + 10}px`; const a = this.getImageDataFromCanvas(this.canvas, [ t * s - 100 / 2, e * s - 100 / 2, 100, 100 ]), o = this.magnifierCanvas.getContext("2d", { willReadFrequently: !0 }).createImageData(this.magnifierCanvas.width, this.magnifierCanvas.height); let c = 0; for (let r = 0; r < 100; r += 1) for (let l = 0; l < 100; l += 1) { for (let I = r; I < r + 1; I++) for (let p = l; p < l + 1; p++) { const n = (I * 100 + p) * 4; o.data[n] = a.data[c], o.data[n + 1] = a.data[c + 1], o.data[n + 2] = a.data[c + 2], o.data[n + 3] = a.data[c + 3]; } c += 4; } this.magnifierCanvas.getContext("2d", { willReadFrequently: !0 }).putImageData(o, 0, 0), this.magnifierCtx.strokeStyle = "rgba(0, 0, 0, 0.5)", this.magnifierCtx.lineWidth = 2, this.magnifierCtx.strokeRect(0, 0, 100, 100); } } /** 销毁放大镜 */ destroyMagnifier() { this.magnifierCanvas && (this.magnifierCanvas.remove(), this.magnifierCanvas = null, this.magnifierCtx = null); } /* 提取像素信息 */ getImageDataFromCanvas(t, [e, i, s, a]) { return t.getContext("2d", { willReadFrequently: !0 }).getImageData(e, i, s, a); } /** 合成事件 */ mergeEvent(t) { var o, c; let e = 0, i = 0, s = 0, a = 0; if (this.isMobile && t.touches) { const { clientX: r, clientY: l } = t.touches[0], I = t.target, { left: p, top: n } = I.getBoundingClientRect(); if (e = Math.round(r - p), i = Math.round(l - n), ((o = t.touches) == null ? void 0 : o.length) === 2) { const { clientX: d = 0, clientY: u = 0 } = t.touches[1] || {}; s = Math.round(Math.abs((d - r) / 2 + r) - p), a = Math.round(Math.abs((u - l) / 2 + l) - n); } else ((c = t.touches) == null ? void 0 : c.length) === 1 && (s = Math.round(r - p), a = Math.round(l - n)); } else e = t.offsetX, i = t.offsetY; return { ...t, mouseX: e, mouseY: i, mouseCX: s, mouseCY: a }; } handleLoad() { this.emit("load", this.image.src), this.IMAGE_ORIGIN_WIDTH = this.IMAGE_WIDTH = this.image.width, this.IMAGE_ORIGIN_HEIGHT = this.IMAGE_HEIGHT = this.image.height, this.fitZoom(); } handleContextmenu(t) { t.preventDefault(), this.evt = t, this.lock; } handleMousewheel(t) { if (t.stopPropagation(), this.evt = t, this.lock || !this.scrollZoom) return; const { mouseX: e, mouseY: i } = this.mergeEvent(t); this.mouse = [e, i], this.setScale(t.deltaY < 0, !0); } handleMouseDown(t) { var r, l, I; if (t.stopPropagation(), this.evt = t, this.lock) return; const { mouseX: e, mouseY: i, mouseCX: s, mouseCY: a } = this.mergeEvent(t), o = Math.round(e / this.scale), c = Math.round(i / this.scale); if (this.mouse = this.isMobile && ((r = t.touches) == null ? void 0 : r.length) === 2 ? [s, a] : [e, i], this.remmberOrigin = [e - this.originX, i - this.originY], !this.isMobile && t.buttons === 1 || this.isMobile && ((l = t.touches) == null ? void 0 : l.length) === 1) { const p = this.activeShape.ctrlsData || []; if (this.ctrlIndex = p.findIndex((n) => this.isPointInCircle(this.mouse, n, this.ctrlRadius)), this.ctrlIndex > -1 && !this.readonly) { const [n, d] = p[this.ctrlIndex]; this.activeShape.type === 2 && this.activeShape.coor.length > 2 && this.ctrlIndex === 0 && this.handleDblclick(t), this.remmber = [[o - n, c - d]]; } else if (this.isInBackground(t)) { if (this.activeShape.creating && !this.readonly) { if ([ 2, 4 /* Line */ ].includes(this.activeShape.type)) { const [n, d] = this.activeShape.coor[this.activeShape.coor.length - 1]; if (n !== o && d !== c) { const u = Math.round(o - this.originX / this.scale), f = Math.round(c - this.originY / this.scale); this.activeShape.coor.push([u, f]); } } } else if (this.createType !== 0 && !this.readonly && !this.isCtrlKey) { let n; const d = Math.round(o - this.originX / this.scale), u = Math.round(c - this.originY / this.scale), f = [d, u]; switch (this.createType) { case 1: n = new T({ coor: [f, f] }, this.dataset.length), n.creating = !0; break; case 2: n = new C({ coor: [f] }, this.dataset.length), n.creating = !0; break; case 3: n = new D({ coor: f }, this.dataset.length), this.emit("add", n); break; case 4: n = new k({ coor: [f] }, this.dataset.length), n.creating = !0; break; case 5: n = new _({ coor: f }, this.dataset.length), n.creating = !0; break; case 6: n = new W({ coor: [f, f] }, this.dataset.length), n.creating = !0; break; } this.dataset.forEach((y) => { y.active = !1; }), n.active = !0, this.dataset.push(n); } else { const [n, d] = this.hitOnShape(this.mouse); if (n > -1) { if (d.dragging = !0, this.dataset.forEach((u, f) => u.active = f === n), this.dataset.splice(n, 1), this.dataset.push(d), !this.readonly) if (this.remmber = [], [ 3, 5 /* Circle */ ].includes(d.type)) { const [u, f] = d.coor; this.remmber = [[o - u, c - f]]; } else d.coor.forEach((u) => { this.remmber.push([o - u[0], c - u[1]]); }); this.emit("select", d); } else this.activeShape.active = !1, this.dataset.sort((u, f) => u.index - f.index), this.emit("select", null); } this.update(); } } else if (!this.isMobile && t.buttons === 2 || this.isMobile && ((I = t.touches) == null ? void 0 : I.length) === 3 && !this.readonly) { if ([ 6 /* Grid */ ].includes(this.activeShape.type) && this.gridMenuEnable) { const p = prompt("x 行 y 列 x,y", [this.activeShape.row, this.activeShape.col].join(",")); if (typeof p == "string") { const [n, d] = p.split(","); /^[1-9]\d*$/.test(n) && /^[1-9]\d*$/.test(d) && (this.activeShape.row = Number(n), this.activeShape.col = Number(d), this.update()); } } this.emit("contextmenu", t); } } handleMouseMove(t) { var r, l, I, p; if (t.stopPropagation(), this.evt = t, this.lock) return; const { mouseX: e, mouseY: i, mouseCX: s, mouseCY: a } = this.mergeEvent(t), o = Math.round(e / this.scale), c = Math.round(i / this.scale); if (this.mouse = this.isMobile && ((r = t.touches) == null ? void 0 : r.length) === 2 ? [s, a] : [e, i], (!this.isMobile && t.buttons === 1 || this.isMobile && ((l = t.touches) == null ? void 0 : l.length) === 1) && this.activeShape.type) { if (this.ctrlIndex > -1 && this.remmber.length && (this.isInBackground(t) || this.activeShape.type === 5)) { const [[n, d]] = this.remmber; if ([ 1, 6 /* Grid */ ].includes(this.activeShape.type)) { const [[u, f], [y, S]] = this.activeShape.coor; let x = []; switch (this.ctrlIndex) { case 0: x = [[o - n, c - d], [y, S]]; break; case 1: x = [[u, c - d], [y, S]]; break; case 2: x = [[u, c - d], [o - n, S]]; break; case 3: x = [[u, f], [o - n, S]]; break; case 4: x = [[u, f], [o - n, c - d]]; break; case 5: x = [[u, f], [y, c - d]]; break; case 6: x = [[o - n, f], [y, c - d]]; break; case 7: x = [[o - n, f], [y, S]]; break; } let [[m, H], [M, b]] = x; (m < 0 || M < 0 || H < 0 || b < 0 || M > this.IMAGE_ORIGIN_WIDTH || b > this.IMAGE_ORIGIN_HEIGHT) && (m < 0 && (m = 0), M < 0 && (M = 0), H < 0 && (H = 0), b < 0 && (b = 0), M > this.IMAGE_ORIGIN_WIDTH && (M = this.IMAGE_ORIGIN_WIDTH), b > this.IMAGE_ORIGIN_HEIGHT && (b = this.IMAGE_ORIGIN_HEIGHT)), M - m >= this.MIN_WIDTH && b - H >= this.MIN_HEIGHT ? this.activeShape.coor = [[m, H], [M, b]] : this.emit("warn", `Width cannot be less than ${this.MIN_WIDTH},Height cannot be less than${this.MIN_HEIGHT}。`); } else if ([ 2, 4 /* Line */ ].includes(this.activeShape.type)) { const u = Math.round(o - this.originX / this.scale), f = Math.round(c - this.originY / this.scale), y = [u, f]; this.activeShape.coor.splice(this.ctrlIndex, 1, y); } else if (this.activeShape.type === 5) { const f = Math.round(o - this.originX / this.scale) - this.activeShape.coor[0]; f >= this.MIN_RADIUS && (this.activeShape.radius = f); } if (this.isMagnifierVisible) { const [u, f] = this.isMobile ? [s, a] : [e, i]; this.createMagnifier(u, f); } } else if (this.activeShape.dragging && !this.readonly) { if (this.isMagnifierVisible && this.activeShape.type === 3) { const [y, S] = this.isMobile ? [s, a] : [e, i]; this.createMagnifier(y, S); } let n = [], d = !0; const u = this.IMAGE_ORIGIN_WIDTH || this.WIDTH, f = this.IMAGE_ORIGIN_HEIGHT || this.HEIGHT; if ([ 3, 5 /* Circle */ ].includes(this.activeShape.type)) { const [y, S] = this.remmber[0], x = o - y, m = c - S; (x < 0 || x > u || m < 0 || m > f) && (d = !1), n = [x, m]; } else for (let y = 0; y < this.activeShape.coor.length; y++) { const S = this.remmber[y], x = o - S[0], m = c - S[1]; (x < 0 || x > u || m < 0 || m > f) && (d = !1), n.push([x, m]); } d && (this.activeShape.coor = n); } else if (this.activeShape.creating && this.isInBackground(t)) { const n = Math.round(o - this.originX / this.scale), d = Math.round(c - this.originY / this.scale); if ([ 1, 6 /* Grid */ ].includes(this.activeShape.type)) this.activeShape.coor.splice(1, 1, [n, d]); else if (this.activeShape.type === 5) { const [u, f] = this.activeShape.coor, y = Math.sqrt((u - n) ** 2 + (f - d) ** 2); this.activeShape.radius = y; } } this.update(); } else if ([ 2, 4 /* Line */ ].includes(this.activeShape.type) && this.activeShape.creating) this.update(); else if (!this.isMobile && t.buttons === 2 && t.which === 3 || this.isMobile && ((I = t.touches) == null ? void 0 : I.length) === 1 && !this.isTouch2) this.originX = Math.round(e - this.remmberOrigin[0]), this.originY = Math.round(i - this.remmberOrigin[1]), this.update(); else if (this.isMobile && ((p = t.touches) == null ? void 0 : p.length) === 2) { this.isTouch2 = !0; const n = t.touches[0], d = t.touches[1], u = this.scaleTouchStore; this.scaleTouchStore = Math.abs((d.clientX - n.clientX) * (d.clientY - n.clientY)), this.setScale(this.scaleTouchStore > u, !0); } } handleMouseUp(t) { var e; if (t.stopPropagation(), this.evt = t, !this.lock) { if (this.destroyMagnifier(), this.isMobile) { if (((e = t.touches) == null ? void 0 : e.length) === 0 && (this.isTouch2 = !1), Date.now() - this.dblTouchStore < this.dblTouch) { this.handleDblclick(t); return; } this.dblTouchStore = Date.now(); } if (this.remmber = [], this.activeShape.type !== 0 && !this.isCtrlKey && (this.activeShape.dragging = !1, this.activeShape.creating)) { if ([ 1, 6 /* Grid */ ].includes(this.activeShape.type)) { const [[i, s], [a, o]] = this.activeShape.coor; Math.abs(i - a) < this.MIN_WIDTH || Math.abs(s - o) < this.MIN_HEIGHT ? (this.dataset.pop(), this.emit("warn", `Width cannot be less than ${this.MIN_WIDTH},Height cannot be less than ${this.MIN_HEIGHT}`)) : (this.activeShape.coor = [[Math.min(i, a), Math.min(s, o)], [Math.max(i, a), Math.max(s, o)]], this.activeShape.creating = !1, this.emit("add", this.activeShape)); } else this.activeShape.type === 5 && (this.activeShape.radius < this.MIN_RADIUS ? (this.dataset.pop(), this.emit("warn", `Radius cannot be less than ${this.MIN_WIDTH}`)) : (this.activeShape.creating = !1, this.emit("add", this.activeShape))); this.update(); } } } handleDblclick(t) { if (t.stopPropagation(), this.evt = t, !this.lock) if ([ 2, 4 /* Line */ ].includes(this.activeShape.type)) { const e = this.activeShape.type === 2 && this.activeShape.coor.length > 2, i = this.activeShape.type === 4 && this.activeShape.coor.length > 1; (e || i) && (this.emit("add", this.activeShape), this.activeShape.creating = !1, this.update()); } else [ 6 /* Grid */ ].includes(this.activeShape.type) && this.activeShape.active && (this.activeShape.gridRects.forEach((e) => { if (this.isPointInRect(this.mouse, e.coor)) { const i = this.activeShape.selected.findIndex((s) => e.index === s); i > -1 ? this.activeShape.selected.splice(i, 1) : this.activeShape.selected.push(e.index); } }), this.update()); } handleKeydown(t) { t.code === this.ctrlCode && (this.isCtrlKey = !0); } handleKeyup(t) { t.code === this.ctrlCode && (this.isCtrlKey = !1), this.evt = t, !(this.lock || document.activeElement !== document.body || this.readonly) && this.activeShape.type && ([ 2, 4 /* Line */ ].includes(this.activeShape.type) && t.key === "Escape" ? (this.activeShape.coor.length > 1 && this.activeShape.creating ? this.activeShape.coor.pop() : this.deleteByIndex(this.activeShape.index), this.update()) : t.key === "Backspace" && this.deleteByIndex(this.activeShape.index)); } /** 初始化配置 */ initSetting() { const t = window.devicePixelRatio || 1; this.image.crossOrigin = "anonymous", this.canvas.style.userSelect = "none", this.ctx = this.ctx || this.canvas.getContext("2d", { alpha: this.alpha }), this.WIDTH = Math.round(this.canvas.clientWidth), this.HEIGHT = Math.round(this.canvas.clientHeight), this.canvas.width = this.WIDTH * t, this.canvas.height = this.HEIGHT * t, this.canvas.style.width = this.WIDTH + "px", this.canvas.style.height = this.HEIGHT + "px", this.offScreen.width = this.WIDTH, this.offScreen.height = this.HEIGHT, this.offScreenCtx = this.offScreenCtx || this.offScreen.getContext("2d", { willReadFrequently: !0 }), this.ctx.scale(t, t); } /** 初始化事件 */ initEvents() { this.image.addEventListener("load", this.handleLoad), this.canvas.addEventListener("touchstart", this.handleMouseDown), this.canvas.addEventListener("touchmove", this.handleMouseMove), this.canvas.addEventListener("touchend", this.handleMouseUp), this.canvas.addEventListener("contextmenu", this.handleContextmenu), this.canvas.addEventListener("mousewheel", this.handleMousewheel), this.canvas.addEventListener("wheel", this.handleMousewheel), this.canvas.addEventListener("mousedown", this.handleMouseDown), this.canvas.addEventListener("mousemove", this.handleMouseMove), this.canvas.addEventListener("mouseup", this.handleMouseUp), this.canvas.addEventListener("dblclick", this.handleDblclick), document.body.addEventListener("keydown", this.handleKeydown, !0), document.body.addEventListener("keyup", this.handleKeyup, !0); } /** * 添加/切换图片 * @param url 图片链接 */ setImage(t) { this.image.src = t; } /** * 设置数据 * @param data Array */ setData(t) { setTimeout(() => { const e = []; t.forEach((i, s) => { if (Object.prototype.toString.call(i).includes("Object")) { let a; switch (i.type) { case 1: a = new T(i, s); break; case 2: a = new C(i, s); break; case 3: a = new D(i, s); break; case 4: a = new k(i, s); break; case 5: a = new _(i, s); break; case 6: a = new W(i, s); break; default: console.warn("Invalid shape", i); break; } [ 1, 2, 3, 4, 5, 6 /* Grid */ ].includes(i.type) && e.push(a); } else console.warn("Shape must be an enumerable Object.", i); }), this.dataset = e, this.update(); }); } /** * 判断是否在标注实例上 * @param mousePoint 点击位置 * @returns */ hitOnShape(t) { let e = -1, i; for (let s = this.dataset.length - 1; s > -1; s--) { const a = this.dataset[s]; if (!a.hide && (a.type === 3 && this.isPointInCircle(t, a.coor, this.ctrlRadius) || a.type === 5 && this.isPointInCircle(t, a.coor, a.radius * this.scale) || a.type === 1 && this.isPointInRect(t, a.coor) || a.type === 2 && this.isPointInPolygon(t, a.coor) || a.type === 4 && this.isPointInLine(t, a.coor) || a.type === 6 && this.isPointInRect(t, a.coor))) { if (this.focusMode && !a.active) continue; e = s, i = a; break; } } return [e, i]; } /** * 判断鼠标是否在背景图内部 * @param e MouseEvent * @returns 布尔值 */ isInBackground(t) { const { mouseX: e, mouseY: i } = this.mergeEvent(t); return e >= this.originX && i >= this.originY && e <= this.originX + this.IMAGE_ORIGIN_WIDTH * this.scale && i <= this.originY + this.IMAGE_ORIGIN_HEIGHT * this.scale; } /** * 判断是否在矩形内 * @param point 坐标 * @param coor 区域坐标 * @returns 布尔值 */ isPointInRect(t, e) { const [i, s] = t, [[a, o], [c, r]] = e.map((l) => l.map((I) => I * this.scale)); return a + this.originX <= i && i <= c + this.originX && o + this.originY <= s && s <= r + this.originY; } /** * 判断是否在多边形内 * @param point 坐标 * @param coor 区域坐标 * @returns 布尔值 */ isPointInPolygon(t, e) { this.offScreenCtx.save(), this.offScreenCtx.clearRect(0, 0, this.WIDTH, this.HEIGHT), this.offScreenCtx.translate(this.originX, this.originY), this.offScreenCtx.beginPath(), e.forEach((a, o) => { const [c, r] = a.map((l) => Math.round(l * this.scale)); o === 0 ? this.offScreenCtx.moveTo(c, r) : this.offScreenCtx.lineTo(c, r); }), this.offScreenCtx.closePath(), this.offScreenCtx.fill(); const i = this.offScreenCtx.getImageData(0, 0, this.WIDTH, this.HEIGHT), s = (t[1] - 1) * this.WIDTH * 4 + t[0] * 4; return this.offScreenCtx.restore(), i.data[s + 3] !== 0; } /** * 判断是否在圆内 * @param point 坐标 * @param center 圆心 * @param r 半径 * @param needScale 是否为圆形点击检测 * @returns 布尔值 */ isPointInCircle(t, e, i) { const [s, a] = t, [o, c] = e.map((l) => l * this.scale); return Math.sqrt((o + this.originX - s) ** 2 + (c + this.originY - a) ** 2) <= i; } /** * 判断是否在折线内 * @param point 坐标 * @param coor 区域坐标 * @returns 布尔值 */ isPointInLine(t, e) { this.offScreenCtx.save(), this.offScreenCtx.clearRect(0, 0, this.WIDTH, this.HEIGHT), this.offScreenCtx.translate(this.originX, this.originY), this.offScreenCtx.lineWidth = this.lineWidth > 5 ? this.lineWidth : 5, this.offScreenCtx.beginPath(), e.forEach((a, o) => { const [c, r] = a.map((l) => Math.round(l * this.scale)); o === 0 ? this.offScreenCtx.moveTo(c, r) : this.offScreenCtx.lineTo(c, r); }), this.offScreenCtx.stroke(); const i = this.offScreenCtx.getImageData(0, 0, this.WIDTH, this.HEIGHT), s = (t[1] - 1) * this.WIDTH * 4 + t[0] * 4; return this.offScreenCtx.restore(), i.data[s + 3] !== 0; } /** * 判断是图形是否属于嵌套关系 (目前只支持矩形和多边形) * @param shape1 标注实例 * @param shape2 标注实例 * @returns 布尔值 */ isNested(t, e) { return X(t, e); } /** * 绘制矩形 * @param shape 标注实例 * @returns */ drawRect(t, e) { if (t.coor.length !== 2) return; const { strokeStyle: i, fillStyle: s, active: a, creating: o, coor: c, lineWidth: r } = t, [[l, I], [p, n]] = c.map((f) => f.map((y) => Math.round(y * this.scale))); this.ctx.save(), this.ctx.lineWidth = r || this.lineWidth, this.ctx.fillStyle = e != null && e.isSelected ? e == null ? void 0 : e.selectedFillStyle : s || this.fillStyle, this.ctx.strokeStyle = a || o ? this.activeStrokeStyle : i || this.strokeStyle; const d = p - l, u = n - I; o || this.ctx.fillRect(l, I, d, u), this.ctx.strokeRect(l, I, d, u), this.ctx.restore(), this.drawLabel(c[0], t); } /** * 绘制多边形 * @param shape 标注实例 */ drawPolygon(t) { const { strokeStyle: e, fillStyle: i, active: s, creating: a, coor: o, lineWidth: c } = t; if (this.ctx.save(), this.ctx.lineJoin = "round", this.ctx.lineWidth = c || this.lineWidth, this.ctx.fillStyle = i || this.fillStyle, this.ctx.strokeStyle = s || a ? this.activeStrokeStyle : e || this.strokeStyle, this.ctx.beginPath(), o.forEach((r, l) => { const [I, p] = r.map((n) => Math.round(n * this.scale)); l === 0 ? this.ctx.moveTo(I, p) : this.ctx.lineTo(I, p); }), a) { const [r, l] = this.mouse || []; this.ctx.lineTo(r - this.originX, l - this.originY); } else o.length > 2 && this.ctx.closePath(); this.ctx.fill(), this.ctx.stroke(), this.ctx.restore(), this.drawLabel(o[0], t); } /** * 绘制点 * @param shape 标注实例 */ drawDot(t) { const { strokeStyle: e, fillStyle: i, active: s, coor: a, lineWidth: o } = t, [c, r] = a.map((l) => l * this.scale); this.ctx.save(), this.ctx.lineWidth = o || this.lineWidth, this.ctx.fillStyle = i || this.ctrlFillStyle, this.ctx.strokeStyle = s ? this.activeStrokeStyle : e || this.strokeStyle, this.ctx.beginPath(), this.ctx.arc(c, r, this.ctrlRadius, 0, 2 * Math.PI, !0), this.ctx.fill(), this.ctx.arc(c, r, this.ctrlRadius, 0, 2 * Math.PI, !0), this.ctx.stroke(), this.ctx.restore(), this.drawLabel(a, t); } /** * 绘制圆 * @param shape 标注实例 */ drawCirle(t) { const { strokeStyle: e, fillStyle: i, active: s, coor: a, label: o, creating: c, radius: r, ctrlsData: l, lineWidth: I } = t, [p, n] = a.map((d) => d * this.scale); this.ctx.save(), this.ctx.lineWidth = I || this.lineWidth, this.ctx.fillStyle = i || this.fillStyle, this.ctx.strokeStyle = s || c ? this.activeStrokeStyle : e || this.strokeStyle, this.ctx.beginPath(), this.ctx.arc(p, n, r * this.scale, 0, 2 * Math.PI, !0), this.ctx.fill(), this.ctx.arc(p, n, r * this.scale, 0, 2 * Math.PI, !0), this.ctx.stroke(), this.ctx.restore(), this.drawLabel(l[0], t); } /** * 绘制折线 * @param shape 标注实例 */ drawLine(t) { const { strokeStyle: e, active: i, creating: s, coor: a, lineWidth: o } = t; if (this.ctx.save(), this.ctx.lineJoin = "round", this.ctx.lineWidth = o || this.lineWidth, this.ctx.strokeStyle = i || s ? this.activeStrokeStyle : e || this.strokeStyle, this.ctx.beginPath(), a.forEach((c, r) => { const [l, I] = c.map((p) => Math.round(p * this.scale)); r === 0 ? this.ctx.moveTo(l, I) : this.ctx.lineTo(l, I); }), s) { const [c, r] = this.mouse || []; this.ctx.lineTo(c - this.originX, r - this.originY); } this.ctx.stroke(), this.ctx.restore(), this.drawLabel(a[0], t); } /** * 绘制网格 * @param shape 标注实例 * @returns */ drawGrid(t) { if (t.coor.length !== 2) return; const { strokeStyle: e, fillStyle: i, active: s, creating: a, coor: o, lineWidth: c } = t, [[r, l], [I, p]] = o.map((u) => u.map((f) => Math.round(f * this.scale))); this.ctx.save(), this.ctx.lineWidth = c || this.lineWidth, this.ctx.fillStyle = i || this.fillStyle, this.ctx.strokeStyle = s || a ? this.activeStrokeStyle : e || this.strokeStyle, t.gridRects.forEach((u, f) => { var y; this.drawRect(u, { selectedFillStyle: t.selectedFillStyle || this.gridSelectedFillStyle, isSelected: (y = t.selected) == null ? void 0 : y.includes(f) }); }); const n = I - r, d = p - l; a || this.ctx.fillRect(r, l, n, d), this.ctx.strokeRect(r, l, n, d), this.ctx.restore(), this.drawLabel(o[0], t); } /** * 绘制控制点 * @param point 坐标 */ drawCtrl(t) { const [e, i] = t.map((s) => s * this.scale); this.ctx.save(), this.ctx.beginPath(), this.ctx.fillStyle = this.ctrlFillStyle, this.ctx.strokeStyle = this.ctrlStrokeStyle, this.ctx.arc(e, i, this.ctrlRadius, 0, 2 * Math.PI, !0), this.ctx.fill(), this.ctx.arc(e, i, this.ctrlRadius, 0, 2 * Math.PI, !0), this.ctx.stroke(), this.ctx.restore(); } /** * 绘制控制点列表 * @param shape 标注实例 */ drawCtrlList(t) { t.ctrlsData.forEach((e, i) => { t.type === 5 ? i === 1 && this.drawCtrl(e) : this.drawCtrl(e); }); } /** * 绘制label * @param point 位置 * @param label 文本 */ drawLabel(t, e) { const { label: i = "", labelFillStyle: s = "", labelFont: a = "", textFillStyle: o = "", hideLabel: c, labelUp: r, lineWidth: l } = e, I = typeof c == "boolean" ? c : this.hideLabel, p = typeof r == "boolean" ? r : this.labelUp, n = l || this.lineWidth; if (i.length && !I) { this.ctx.font = a || this.labelFont; const d = 4, u = 4, f = i.length < this.labelMaxLen + 1 ? i : `${i.slice(0, this.labelMaxLen)}...`, y = this.ctx.measureText(f), S = parseInt(this.ctx.font) - 4, x = y.width + d * 2, m = S + u * 2, [H, M] = t.map((L) => L * this.scale), b = this.IMAGE_ORIGIN_WIDTH - t[0] < x / this.scale, A = this.IMAGE_ORIGIN_HEIGHT - t[1] < m / this.scale, R = t[1] > m / this.scale, G = p ? R : A; this.ctx.save(), this.ctx.fillStyle = s || this.labelFillStyle, this.ctx.fillRect(b ? H - y.width - d - n / 2 : H + n / 2, G ? M - m - n / 2 : M + n / 2, x, m), this.ctx.fillStyle = o || this.textFillStyle, this.ctx.fillText(f, b ? H - y.width : H + d + n / 2, G ? M - m + S + u : M + S + u + n / 2, 180), this.ctx.restore(); } } /** * 更新画布 */ update() { window.cancelAnimationFrame(this.timer), this.timer = window.requestAnimationFrame(() => { this.ctx.save(), this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT), this.ctx.translate(this.originX, this.originY), this.IMAGE_WIDTH && this.IMAGE_HEIGHT && this.ctx.drawImage(this.image, 0, 0, this.IMAGE_WIDTH, this.IMAGE_HEIGHT); const t = this.focusMode ? this.activeShape.type ? [this.activeShape] : [] : this.dataset; for (let e = 0; e < t.length; e++) { const i = t[e]; if (!i.hide) switch (i.type) { case 1: this.drawRect(i); break; case 2: this.drawPolygon(i); break; case 3: this.drawDot(i); break; case 4: this.drawLine(i); break; case 5: this.drawCirle(i); break; case 6: this.drawGrid(i); break; } } [ 1, 2, 4, 5, 6 /* Grid */ ].includes(this.activeShape.type) && !this.activeShape.hide && this.drawCtrlList(this.activeShape), this.ctx.restore(), this.emit("updated", this.dataset); }); } /** * 删除指定矩形 * @param index number */ deleteByIndex(t) { const e = this.dataset.findIndex((i) => i.index === t); e > -1 && (this.emit("delete", this.dataset[e]), this.dataset.splice(e, 1), this.dataset.forEach((i, s) => { i.index = s; }), this.update()); } /** * 计算缩放步长 */ calcStep(t = "") { this.IMAGE_WIDTH < this.WIDTH && this.IMAGE_HEIGHT < this.HEIGHT && (t === "" || t === "b") && (this.setScale(!0, !1, !0), this.calcStep("b")), (this.IMAGE_WIDTH > this.WIDTH || this.IMAGE_HEIGHT > this.HEIGHT) && (t === "" || t === "s") && (this.setScale(!1, !1, !0), this.calcStep("s")); } /** * 缩放 * @param type true放大5%,false缩小5% * @param center 缩放中心 center|mouse * @param pure 不绘制 */ setScale(t, e = !1, i = !1) { if (this.lock || !t && this.imageMin < 20 || t && this.IMAGE_WIDTH > this.imageOriginMax * 100) return; t ? this.scaleStep++ : this.scaleStep--; let s = 0, a = 0; const [o, c] = this.mouse || []; e && (s = (o - this.originX) / this.scale, a = (c - this.originY) / this.scale); const r = Math.abs(this.scaleStep), l = this.IMAGE_WIDTH; if (this.IMAGE_WIDTH = Math.round(this.IMAGE_ORIGIN_WIDTH * (this.scaleStep >= 0 ? 1.05 : 0.95) ** r), this.IMAGE_HEIGHT = Math.round(this.IMAGE_ORIGIN_HEIGHT * (this.scaleStep >= 0 ? 1.05 : 0.95) ** r), e) this.originX = o - s * this.scale, this.originY = c - a * this.scale; else { const I = this.IMAGE_WIDTH / l; this.originX = this.WIDTH / 2 - (this.WIDTH / 2 - this.originX) * I, this.originY = this.HEIGHT / 2 - (this.HEIGHT / 2 - this.originY) * I; } i || this.update(); } /** * 适配背景图 */ fitZoom() { this.calcStep(), this.IMAGE_HEIGHT / this.IMAGE_WIDTH >= this.HEIGHT / this.WIDTH ? (this.IMAGE_WIDTH = this.IMAGE_ORIGIN_WIDTH / (this.IMAGE_ORIGIN_HEIGHT / this.HEIGHT), this.IMAGE_HEIGHT = this.HEIGHT) : (this.IMAGE_WIDTH = this.WIDTH, this.IMAGE_HEIGHT = this.IMAGE_ORIGIN_HEIGHT / (this.IMAGE_ORIGIN_WIDTH / this.WIDTH)), this.originX = (this.WIDTH - this.IMAGE_WIDTH) / 2, this.originY = (this.HEIGHT - this.IMAGE_HEIGHT) / 2, this.update(); } /** * 设置专注模式 * @param type {boolean} */ setFocusMode(t) { this.focusMode = t, this.update(); } /** * 销毁 */ destroy() { this.image.removeEventListener("load", this.handleLoad), this.canvas.removeEventListener("contextmenu", this.handleContextmenu), this.canvas.removeEventListener("mousewheel", this.handleMousewheel), this.canvas.removeEventListener("wheel", this.handleMousewheel), this.canvas.removeEventListener("mousedown", this.handleMouseDown), this.canvas.removeEventListener("touchend", this.handleMouseDown), this.canvas.removeEventListener("mousemove", this.handleMouseMove), this.canvas.removeEventListener("touchmove", this.handleMouseMove), this.canvas.removeEventListener("mouseup", this.handleMouseUp), this.canvas.removeEventListener("touchend", this.handleMouseUp), this.canvas.removeEventListener("dblclick", this.handleDblclick), document.body.removeEventListener("keydown", this.handleKeydown, !0), document.body.removeEventListener("keyup", this.handleKeyup, !0), this.canvas.width = this.WIDTH, this.canvas.height = this.HEIGHT, this.canvas.style.width = null, this.canvas.style.height = null, this.canvas.style.userSelect = null; } /** * 重新设置画布大小 */ resize() { this.canvas.width = null, this.canvas.height = null, this.canvas.style.width = null, this.canvas.style.height = null, this.initSetting(), this.update(); } } export { nt as default };