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
JavaScript
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
};