UNPKG

ml-canvas

Version:

A Vue.js canvas component designed for machine learning annotation tasks and AI tool development

591 lines (586 loc) 21.4 kB
import { ref as d, computed as ze, watch as te, onMounted as Ee, nextTick as ae, onUnmounted as je, createElementBlock as Be, openBlock as Ae, createElementVNode as q, normalizeStyle as Fe, normalizeClass as Ne } from "vue"; function qe() { const S = ` /* Component styles - using global scope for fixed positioned elements */ .ml-canvas-container { width: 100%; height: 100%; position: relative; } .ml-canvas { display: block; width: 100%; height: 100%; } .magnifier { position: fixed; width: 200px; height: 200px; border: 3px solid #667eea; border-radius: 50%; pointer-events: none; display: none; z-index: 1000; box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); background: white; overflow: hidden; } .magnifier.visible { display: block; } .magnifier-canvas { position: absolute; width: auto; height: auto; cursor: none; }`; if (typeof document < "u") { const Y = document.createElement("style"); Y.textContent = S, document.head.appendChild(Y); } } let le = !1; function Ve() { le || (qe(), le = !0); } const $e = ["width", "height"], Je = { __name: "MLCanvas", props: { pasteEnabled: { type: Boolean, default: !0 }, drawingMode: { type: String, default: "none", // 'none', 'rectangle', 'polygon', 'freestyle', 'freeform', 'delete' validator: (S) => ["none", "rectangle", "polygon", "freestyle", "freeform", "delete"].includes(S) }, freestyleSensitivity: { type: Number, default: 1, // Lower values = more points (smoother), higher values = fewer points (coarser) validator: (S) => S >= 0.1 && S <= 10 }, simplificationTolerance: { type: Number, default: 2, // Tolerance for path simplification algorithm validator: (S) => S >= 0.1 && S <= 20 }, magnifierEnabled: { type: Boolean, default: !1 } }, emits: ["shape-created", "shape-removed", "canvas-reset", "image-pasted"], setup(S, { expose: Y, emit: ne }) { const v = S, L = ne, j = d(null), D = d(null), p = d(0), w = d(0), i = d(null), k = d(null), U = d(v.pasteEnabled), W = d(!1), B = d({ x: 0, y: 0 }), C = d({ x: 0, y: 0 }), y = d([]), g = d([]), m = d([]), P = d(null); let ie = 0; const H = d(null), A = d(null), x = d(null), _ = d(!1); let X = null; const oe = ze(() => { switch (v.drawingMode) { case "delete": return "not-allowed"; case "rectangle": case "polygon": case "freestyle": case "freeform": return "crosshair"; case "none": default: return "default"; } }), V = () => { if (!j.value) return; const t = j.value.getBoundingClientRect(), a = Math.floor(t.width), l = Math.floor(t.height); a <= 0 || l <= 0 || (p.value = a, w.value = l, ae(() => { if (D.value) { const n = D.value; n.width = a, n.height = l; const s = n.getContext("2d"); i.value = s, O(), M(); } })); }, $ = () => { X && clearTimeout(X), X = setTimeout(V, 100); }, O = () => { if (!A.value) return; const e = A.value; e.width = 200, e.height = 200, x.value = e.getContext("2d"); }, se = (e) => { if (!v.magnifierEnabled || !x.value || !D.value) { _.value = !1; return; } const t = D.value, a = t.getBoundingClientRect(), l = t.width / a.width, n = t.height / a.height, s = (e.clientX - a.left) * l, r = (e.clientY - a.top) * n; _.value = !0, H.value && (H.value.style.left = e.clientX + 20 + "px", H.value.style.top = e.clientY - 110 + "px", e.clientX > window.innerWidth - 250 && (H.value.style.left = e.clientX - 220 + "px"), e.clientY < 150 && (H.value.style.top = e.clientY + 20 + "px")); const o = 200 / 3; x.value.clearRect(0, 0, 200, 200), x.value.drawImage( t, Math.max(0, s - o / 2), Math.max(0, r - o / 2), o, o, 0, 0, 200, 200 ), x.value.strokeStyle = "#667eea", x.value.lineWidth = 1, x.value.beginPath(), x.value.moveTo(100, 0), x.value.lineTo(100, 200), x.value.moveTo(0, 100), x.value.lineTo(200, 100), x.value.stroke(); }, F = async (e, t = 0, a = 0, l = null, n = null, s = !0) => { if (i.value) return new Promise((r, c) => { const o = new Image(); o.onload = () => { let h = l || o.naturalWidth, u = n || o.naturalHeight; if (s && (l === null || n === null)) { const f = p.value / w.value, b = o.naturalWidth / o.naturalHeight; b > f ? (h = p.value, u = p.value / b) : (u = w.value, h = w.value * b), t = (p.value - h) / 2, a = (w.value - u) / 2; } i.value.drawImage(o, t, a, h, u), k.value = o, P.value = { canvasX: t, canvasY: a, canvasWidth: h, canvasHeight: u, originalWidth: o.naturalWidth, originalHeight: o.naturalHeight }, r({ width: h, height: u, x: t, y: a }); }, o.onerror = c, o.src = e; }); }, re = (e, t, a, l, n = {}) => { if (!i.value) return; const { fillStyle: s = null, strokeStyle: r = "#000000", lineWidth: c = 1, lineDash: o = [] } = n, h = { x: e, y: t, width: a, height: l }, u = R(e, t), f = R(e + a, t + l), b = u && f ? { x: u.x, y: u.y, width: f.x - u.x, height: f.y - u.y } : h, T = I("rectangle", h, b, { fillStyle: s, strokeStyle: r, lineWidth: c, lineDash: o }); return M(), T; }, ue = (e, t = {}) => { if (!i.value || !e || e.length < 3) return; const { fillStyle: a = null, strokeStyle: l = "#000000", lineWidth: n = 1, lineDash: s = [], closePath: r = !0 } = t, c = e.map((u) => R(u.x, u.y)).filter((u) => u !== null), o = c.length > 0 ? c : e, h = I("polygon", e, o, { fillStyle: a, strokeStyle: l, lineWidth: n, lineDash: s, closePath: r }); return M(), h; }, z = () => { i.value && i.value.clearRect(0, 0, p.value, w.value); }, ce = () => `shape_${++ie}_${Date.now()}`, I = (e, t, a = null, l = {}) => { const n = { id: ce(), type: e, canvas: t, image: a || t, style: l, timestamp: Date.now() }; return m.value.push(n), L("shape-created", n), n; }, N = (e) => { if (!i.value || !e) return; const { type: t, canvas: a, style: l } = e; if (t === "rectangle") { const { x: n, y: s, width: r, height: c } = a, { fillStyle: o = null, strokeStyle: h = "#000000", lineWidth: u = 1, lineDash: f = [] } = l; i.value.save(), f.length > 0 && i.value.setLineDash(f), o && (i.value.fillStyle = o, i.value.fillRect(n, s, r, c)), h && (i.value.strokeStyle = h, i.value.lineWidth = u, i.value.strokeRect(n, s, r, c)), i.value.restore(); } else if (t === "polygon" || t === "freestyle" || t === "freeform") { const n = a, { fillStyle: s = null, strokeStyle: r = t === "polygon" ? "#00ff00" : "#0066ff", lineWidth: c = 2, lineDash: o = [], closePath: h = !0 } = l; if (!n || n.length < 2) return; if (i.value.save(), i.value.beginPath(), o.length > 0 && i.value.setLineDash(o), (t === "freestyle" || t === "freeform") && n.length > 2) Q(i.value, n, h); else { i.value.moveTo(n[0].x, n[0].y); for (let u = 1; u < n.length; u++) i.value.lineTo(n[u].x, n[u].y); } h && i.value.closePath(), s && (i.value.fillStyle = s, i.value.fill()), r && (i.value.strokeStyle = r, i.value.lineWidth = c, i.value.stroke()), i.value.restore(); } }, he = async (e = 0, t = 0, a = null, l = null, n = !0) => { if (!U.value) return null; try { const s = await navigator.clipboard.read(); for (const r of s) for (const c of r.types) if (c.startsWith("image/")) { const o = await r.getType(c), h = URL.createObjectURL(o), u = await F(h, e, t, a, l, n), f = new Image(); return f.src = h, k.value = f, u && n && (P.value = { canvasX: u.x, canvasY: u.y, canvasWidth: u.width, canvasHeight: u.height, originalWidth: f.naturalWidth, originalHeight: f.naturalHeight }), URL.revokeObjectURL(h), L("image-pasted", { width: u.width, height: u.height, x: u.x, y: u.y, originalWidth: f.naturalWidth, originalHeight: f.naturalHeight, image: f }), u; } } catch (s) { console.warn("Failed to paste image from clipboard:", s); } return null; }, G = async (e) => { if (!U.value) return; e.preventDefault(); const t = e.clipboardData || e.originalEvent.clipboardData; if (!t) return; const a = t.items; for (let l = 0; l < a.length; l++) { const n = a[l]; if (n.type.startsWith("image/")) { const s = n.getAsFile(); if (s) { const r = URL.createObjectURL(s), c = await F(r, 0, 0, null, null, !0), o = new Image(); return o.src = r, k.value = o, c && (P.value = { canvasX: c.x, canvasY: c.y, canvasWidth: c.width, canvasHeight: c.height, originalWidth: o.naturalWidth, originalHeight: o.naturalHeight }), URL.revokeObjectURL(r), L("image-pasted", { width: c.width, height: c.height, x: c.x, y: c.y, originalWidth: o.naturalWidth, originalHeight: o.naturalHeight, image: o }), c; } } } }, ve = (e) => { U.value = e; }, fe = () => k.value, de = async (e, t = 0, a = 0, l = null, n = null, s = !0) => { if (!(!i.value || !e)) return new Promise((r, c) => { try { let o = l || e.naturalWidth, h = n || e.naturalHeight; if (s && (l === null || n === null)) { const u = p.value / w.value, f = e.naturalWidth / e.naturalHeight; f > u ? (o = p.value, h = p.value / f) : (h = w.value, o = w.value * f), t = (p.value - o) / 2, a = (w.value - h) / 2; } z(), i.value.drawImage(e, t, a, o, h), k.value = e, P.value = { canvasX: t, canvasY: a, canvasWidth: o, canvasHeight: h, originalWidth: e.naturalWidth, originalHeight: e.naturalHeight }, m.value.forEach((u) => { N(u); }), r({ width: o, height: h, x: t, y: a }); } catch (o) { c(o); } }); }, J = (e) => { const t = D.value.getBoundingClientRect(); return { x: e.clientX - t.left, y: e.clientY - t.top }; }, R = (e, t) => { if (!P.value) return { x: e, y: t }; const { canvasX: a, canvasY: l, canvasWidth: n, canvasHeight: s, originalWidth: r, originalHeight: c } = P.value; if (e < a || e > a + n || t < l || t > l + s) return null; const o = (e - a) / n, h = (t - l) / s; return { x: Math.round(o * r), y: Math.round(h * c) }; }, ge = (e) => { const t = J(e); if (v.drawingMode === "delete") { const a = Z(t); a !== null && E(a); return; } v.drawingMode !== "none" && (B.value = t, C.value = t, v.drawingMode === "rectangle" ? W.value = !0 : v.drawingMode === "polygon" ? y.value.push(t) : (v.drawingMode === "freestyle" || v.drawingMode === "freeform") && (W.value = !0, g.value = [t])); }, ye = (e) => { if (se(e), v.drawingMode === "none" || v.drawingMode === "delete") return; const t = J(e); if (C.value = t, v.drawingMode === "rectangle" && W.value) M(), ke(); else if (v.drawingMode === "polygon" && y.value.length > 0) M(), De(); else if ((v.drawingMode === "freestyle" || v.drawingMode === "freeform") && W.value) { const a = g.value[g.value.length - 1], l = Math.sqrt( Math.pow(t.x - a.x, 2) + Math.pow(t.y - a.y, 2) ), n = v.freestyleSensitivity; l > n && g.value.push(t), M(), Re(); } }, me = () => { v.drawingMode === "rectangle" && W.value ? (W.value = !1, Me() && M()) : (v.drawingMode === "freestyle" || v.drawingMode === "freeform") && W.value && (console.log("Freestyle mouse up, path length:", g.value.length), W.value = !1, Se() ? (console.log("Shape created successfully"), g.value = [], M()) : (console.log("Failed to create shape"), g.value = [], M())); }, pe = () => { _.value = !1; }, we = () => { v.drawingMode === "polygon" && y.value.length >= 2 && K(); }, xe = (e) => { e.preventDefault(), v.drawingMode === "polygon" && y.value.length >= 2 && K(); }, K = () => { be() && (y.value = [], M()); }, Me = () => { const e = B.value, t = C.value, a = { x: Math.min(e.x, t.x), y: Math.min(e.y, t.y), width: Math.abs(t.x - e.x), height: Math.abs(t.y - e.y) }, l = R(a.x, a.y), n = R( a.x + a.width, a.y + a.height ); if (!l || !n) return null; const s = { x: l.x, y: l.y, width: n.x - l.x, height: n.y - l.y }; return I("rectangle", a, s, { fillStyle: null, strokeStyle: "#ff0000", lineWidth: 2, lineDash: [] }); }, be = () => { const e = [...y.value], t = e.map((a) => R(a.x, a.y)).filter((a) => a !== null); return t.length < 2 ? null : I("polygon", e, t, { fillStyle: null, strokeStyle: "#00ff00", lineWidth: 2, lineDash: [], closePath: !0 }); }, Se = () => { if (g.value.length < 2) return console.log("Not enough points for freestyle shape:", g.value.length), null; const e = We(g.value, v.simplificationTolerance); console.log( "Original path points:", g.value.length, "Simplified points:", e.length ); const t = e.map((l) => R(l.x, l.y)).filter((l) => l !== null); if (console.log("Image points after scaling:", t.length), t.length < 2) return console.log("Not enough image points after scaling:", t.length), null; console.log("Creating freestyle shape with", t.length, "points"); const a = v.drawingMode === "freeform" ? "freeform" : "freestyle"; return I(a, e, t, { fillStyle: null, strokeStyle: "#0066ff", lineWidth: 2, lineDash: [], closePath: !0 }); }, We = (e, t) => { if (e.length <= 2) return e; const a = t * t, l = (r, c, o, h, u) => { let f = h, b = -1; for (let T = c + 1; T < o; T++) { const ee = Pe(r[T], r[c], r[o]); ee > f && (b = T, f = ee); } f > h && (b - c > 1 && l(r, c, b, h, u), u.push(r[b]), o - b > 1 && l(r, b, o, h, u)); }, n = e.length - 1, s = [e[0]]; return l(e, 0, n, a, s), s.push(e[n]), s; }, Pe = (e, t, a) => { const l = a.x - t.x, n = a.y - t.y; let s, r; if (l !== 0 || n !== 0) { const c = ((e.x - t.x) * l + (e.y - t.y) * n) / (l * l + n * n); c > 1 ? (s = e.x - a.x, r = e.y - a.y) : c > 0 ? (s = e.x - (t.x + l * c), r = e.y - (t.y + n * c)) : (s = e.x - t.x, r = e.y - t.y); } else s = e.x - t.x, r = e.y - t.y; return s * s + r * r; }, ke = () => { const e = B.value, t = C.value, a = Math.min(e.x, t.x), l = Math.min(e.y, t.y), n = Math.abs(t.x - e.x), s = Math.abs(t.y - e.y); i.value.save(), i.value.strokeStyle = "#ff0000", i.value.lineWidth = 2, i.value.setLineDash([5, 5]), i.value.strokeRect(a, l, n, s), i.value.restore(); }, De = () => { if (!(y.value.length < 1)) { i.value.save(), i.value.strokeStyle = "#00ff00", i.value.lineWidth = 2, i.value.setLineDash([5, 5]), i.value.beginPath(), i.value.moveTo(y.value[0].x, y.value[0].y); for (let e = 1; e < y.value.length; e++) i.value.lineTo(y.value[e].x, y.value[e].y); i.value.lineTo(C.value.x, C.value.y), i.value.stroke(), i.value.fillStyle = "#00ff00", y.value.forEach((e) => { i.value.beginPath(), i.value.arc(e.x, e.y, 3, 0, 2 * Math.PI), i.value.fill(); }), i.value.restore(); } }, Re = () => { g.value.length < 2 || (i.value.save(), i.value.strokeStyle = "#0066ff", i.value.lineWidth = 2, i.value.setLineDash([3, 3]), i.value.beginPath(), Q(i.value, g.value, !1), g.value.length > 2 && i.value.closePath(), i.value.stroke(), i.value.restore()); }, Q = (e, t, a = !0) => { if (t.length < 2) return; if (e.moveTo(t[0].x, t[0].y), t.length === 2) { e.lineTo(t[1].x, t[1].y); return; } for (let n = 1; n < t.length - 1; n++) { const s = t[n], r = t[n + 1], c = (s.x + r.x) / 2, o = (s.y + r.y) / 2; e.quadraticCurveTo(s.x, s.y, c, o); } const l = t[t.length - 1]; if (e.lineTo(l.x, l.y), a && t.length > 2) { const n = t[0], s = (l.x + n.x) / 2, r = (l.y + n.y) / 2; e.quadraticCurveTo(l.x, l.y, s, r); } }, M = () => { if (z(), k.value && P.value) { const { canvasX: e, canvasY: t, canvasWidth: a, canvasHeight: l } = P.value; i.value.drawImage(k.value, e, t, a, l); } m.value.forEach((e) => { N(e); }); }, Ce = () => m.value, He = () => { m.value = [], M(); }, Z = (e) => { for (let t = m.value.length - 1; t >= 0; t--) { const a = m.value[t]; if (a.type === "rectangle") { const l = a.canvas; if (e.x >= l.x && e.x <= l.x + l.width && e.y >= l.y && e.y <= l.y + l.height) return a.id; } else if ((a.type === "polygon" || a.type === "freestyle" || a.type === "freeform") && Te(e, a.canvas)) return a.id; } return null; }, Ie = (e) => m.value.find((t) => t.id === e), Te = (e, t) => { let a = !1; const l = e.x, n = e.y; for (let s = 0, r = t.length - 1; s < t.length; r = s++) { const c = t[s].x, o = t[s].y, h = t[r].x, u = t[r].y; o > n != u > n && l < (h - c) * (n - o) / (u - o) + c && (a = !a); } return a; }, E = (e) => { const t = m.value.findIndex((a) => a.id === e); if (t >= 0) { const a = m.value.splice(t, 1)[0]; return M(), L("shape-removed", a), a; } return null; }, Le = (e) => { if (typeof e == "string") return E(e); if (e >= 0 && e < m.value.length) { const t = m.value[e]; return E(t.id); } return null; }, _e = () => { z(), m.value = [], k.value = null, P.value = null, L("canvas-reset"); }; te( () => v.pasteEnabled, (e) => { U.value = e; } ), te( () => v.drawingMode, (e) => { (e === "none" || e === "delete") && (W.value = !1, y.value = [], g.value = []); } ); const Xe = () => i.value, Ye = () => D.value, Ue = () => ({ width: p.value, height: w.value }); return Ee(() => { Ve(), window.addEventListener("resize", $), window.addEventListener("paste", G), V(), ae(() => { O(); }); }), je(() => { window.removeEventListener("resize", $), window.removeEventListener("paste", G), X && clearTimeout(X); }), Y({ addImage: F, drawRectangle: re, drawPolygon: ue, clearCanvas: z, resetCanvas: _e, getContext: Xe, getCanvas: Ye, getCanvasSize: Ue, pasteImage: he, getImage: fe, updateImage: de, setImagePasteEnabled: ve, getDrawnShapes: Ce, clearDrawnShapes: He, removeShape: Le, removeShapeById: E, findShapeAtPosition: Z, findShapeById: Ie, renderShape: N, storeShape: I, setMagnifierEnabled: (e) => { e || (_.value = !1); } }), (e, t) => (Ae(), Be("div", { ref_key: "containerRef", ref: j, class: "ml-canvas-container" }, [ q("canvas", { ref_key: "canvasRef", ref: D, width: p.value, height: w.value, class: "ml-canvas", onMousedown: ge, onMousemove: ye, onMouseup: me, onMouseleave: pe, onDblclick: we, onContextmenu: xe, style: Fe({ cursor: oe.value }) }, null, 44, $e), q("div", { ref_key: "magnifierRef", ref: H, class: Ne(["magnifier", { visible: _.value && v.magnifierEnabled }]) }, [ q("canvas", { ref_key: "magnifierCanvasRef", ref: A, class: "magnifier-canvas" }, null, 512) ], 2) ], 512)); } }; export { Je as default };