ml-canvas
Version:
A Vue.js canvas component designed for machine learning annotation tasks and AI tool development
39 lines (34 loc) • 14.3 kB
JavaScript
(function(h,L){typeof exports=="object"&&typeof module<"u"?module.exports=L(require("vue")):typeof define=="function"&&define.amd?define(["vue"],L):(h=typeof globalThis<"u"?globalThis:h||self,h.MLCanvas=L(h.Vue))})(this,(function(h){"use strict";function L(){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 V=!1;function ae(){V||(L(),V=!0)}const ne=["width","height"];return{__name:"MLCanvas",props:{pasteEnabled:{type:Boolean,default:!0},drawingMode:{type:String,default:"none",validator:S=>["none","rectangle","polygon","freestyle","freeform","delete"].includes(S)},freestyleSensitivity:{type:Number,default:1,validator:S=>S>=.1&&S<=10},simplificationTolerance:{type:Number,default:2,validator:S=>S>=.1&&S<=20},magnifierEnabled:{type:Boolean,default:!1}},emits:["shape-created","shape-removed","canvas-reset","image-pasted"],setup(S,{expose:Y,emit:le}){const d=S,_=le,B=h.ref(null),D=h.ref(null),p=h.ref(0),w=h.ref(0),i=h.ref(null),k=h.ref(null),U=h.ref(d.pasteEnabled),W=h.ref(!1),N=h.ref({x:0,y:0}),C=h.ref({x:0,y:0}),y=h.ref([]),g=h.ref([]),m=h.ref([]),P=h.ref(null);let ie=0;const H=h.ref(null),A=h.ref(null),x=h.ref(null),E=h.ref(!1);let X=null;const oe=h.computed(()=>{switch(d.drawingMode){case"delete":return"not-allowed";case"rectangle":case"polygon":case"freestyle":case"freeform":return"crosshair";case"none":default:return"default"}}),$=()=>{if(!B.value)return;const t=B.value.getBoundingClientRect(),a=Math.floor(t.width),n=Math.floor(t.height);a<=0||n<=0||(p.value=a,w.value=n,h.nextTick(()=>{if(D.value){const l=D.value;l.width=a,l.height=n;const s=l.getContext("2d");i.value=s,G(),M()}}))},O=()=>{X&&clearTimeout(X),X=setTimeout($,100)},G=()=>{if(!A.value)return;const e=A.value;e.width=200,e.height=200,x.value=e.getContext("2d")},se=e=>{if(!d.magnifierEnabled||!x.value||!D.value){E.value=!1;return}const t=D.value,a=t.getBoundingClientRect(),n=t.width/a.width,l=t.height/a.height,s=(e.clientX-a.left)*n,r=(e.clientY-a.top)*l;E.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,n=null,l=null,s=!0)=>{if(i.value)return new Promise((r,u)=>{const o=new Image;o.onload=()=>{let f=n||o.naturalWidth,c=l||o.naturalHeight;if(s&&(n===null||l===null)){const v=p.value/w.value,b=o.naturalWidth/o.naturalHeight;b>v?(f=p.value,c=p.value/b):(c=w.value,f=w.value*b),t=(p.value-f)/2,a=(w.value-c)/2}i.value.drawImage(o,t,a,f,c),k.value=o,P.value={canvasX:t,canvasY:a,canvasWidth:f,canvasHeight:c,originalWidth:o.naturalWidth,originalHeight:o.naturalHeight},r({width:f,height:c,x:t,y:a})},o.onerror=u,o.src=e})},re=(e,t,a,n,l={})=>{if(!i.value)return;const{fillStyle:s=null,strokeStyle:r="#000000",lineWidth:u=1,lineDash:o=[]}=l,f={x:e,y:t,width:a,height:n},c=R(e,t),v=R(e+a,t+n),b=c&&v?{x:c.x,y:c.y,width:v.x-c.x,height:v.y-c.y}:f,I=T("rectangle",f,b,{fillStyle:s,strokeStyle:r,lineWidth:u,lineDash:o});return M(),I},ce=(e,t={})=>{if(!i.value||!e||e.length<3)return;const{fillStyle:a=null,strokeStyle:n="#000000",lineWidth:l=1,lineDash:s=[],closePath:r=!0}=t,u=e.map(c=>R(c.x,c.y)).filter(c=>c!==null),o=u.length>0?u:e,f=T("polygon",e,o,{fillStyle:a,strokeStyle:n,lineWidth:l,lineDash:s,closePath:r});return M(),f},j=()=>{i.value&&i.value.clearRect(0,0,p.value,w.value)},ue=()=>`shape_${++ie}_${Date.now()}`,T=(e,t,a=null,n={})=>{const l={id:ue(),type:e,canvas:t,image:a||t,style:n,timestamp:Date.now()};return m.value.push(l),_("shape-created",l),l},q=e=>{if(!i.value||!e)return;const{type:t,canvas:a,style:n}=e;if(t==="rectangle"){const{x:l,y:s,width:r,height:u}=a,{fillStyle:o=null,strokeStyle:f="#000000",lineWidth:c=1,lineDash:v=[]}=n;i.value.save(),v.length>0&&i.value.setLineDash(v),o&&(i.value.fillStyle=o,i.value.fillRect(l,s,r,u)),f&&(i.value.strokeStyle=f,i.value.lineWidth=c,i.value.strokeRect(l,s,r,u)),i.value.restore()}else if(t==="polygon"||t==="freestyle"||t==="freeform"){const l=a,{fillStyle:s=null,strokeStyle:r=t==="polygon"?"#00ff00":"#0066ff",lineWidth:u=2,lineDash:o=[],closePath:f=!0}=n;if(!l||l.length<2)return;if(i.value.save(),i.value.beginPath(),o.length>0&&i.value.setLineDash(o),(t==="freestyle"||t==="freeform")&&l.length>2)Z(i.value,l,f);else{i.value.moveTo(l[0].x,l[0].y);for(let c=1;c<l.length;c++)i.value.lineTo(l[c].x,l[c].y)}f&&i.value.closePath(),s&&(i.value.fillStyle=s,i.value.fill()),r&&(i.value.strokeStyle=r,i.value.lineWidth=u,i.value.stroke()),i.value.restore()}},he=async(e=0,t=0,a=null,n=null,l=!0)=>{if(!U.value)return null;try{const s=await navigator.clipboard.read();for(const r of s)for(const u of r.types)if(u.startsWith("image/")){const o=await r.getType(u),f=URL.createObjectURL(o),c=await F(f,e,t,a,n,l),v=new Image;return v.src=f,k.value=v,c&&l&&(P.value={canvasX:c.x,canvasY:c.y,canvasWidth:c.width,canvasHeight:c.height,originalWidth:v.naturalWidth,originalHeight:v.naturalHeight}),URL.revokeObjectURL(f),_("image-pasted",{width:c.width,height:c.height,x:c.x,y:c.y,originalWidth:v.naturalWidth,originalHeight:v.naturalHeight,image:v}),c}}catch(s){console.warn("Failed to paste image from clipboard:",s)}return null},J=async e=>{if(!U.value)return;e.preventDefault();const t=e.clipboardData||e.originalEvent.clipboardData;if(!t)return;const a=t.items;for(let n=0;n<a.length;n++){const l=a[n];if(l.type.startsWith("image/")){const s=l.getAsFile();if(s){const r=URL.createObjectURL(s),u=await F(r,0,0,null,null,!0),o=new Image;return o.src=r,k.value=o,u&&(P.value={canvasX:u.x,canvasY:u.y,canvasWidth:u.width,canvasHeight:u.height,originalWidth:o.naturalWidth,originalHeight:o.naturalHeight}),URL.revokeObjectURL(r),_("image-pasted",{width:u.width,height:u.height,x:u.x,y:u.y,originalWidth:o.naturalWidth,originalHeight:o.naturalHeight,image:o}),u}}}},fe=e=>{U.value=e},de=()=>k.value,ve=async(e,t=0,a=0,n=null,l=null,s=!0)=>{if(!(!i.value||!e))return new Promise((r,u)=>{try{let o=n||e.naturalWidth,f=l||e.naturalHeight;if(s&&(n===null||l===null)){const c=p.value/w.value,v=e.naturalWidth/e.naturalHeight;v>c?(o=p.value,f=p.value/v):(f=w.value,o=w.value*v),t=(p.value-o)/2,a=(w.value-f)/2}j(),i.value.drawImage(e,t,a,o,f),k.value=e,P.value={canvasX:t,canvasY:a,canvasWidth:o,canvasHeight:f,originalWidth:e.naturalWidth,originalHeight:e.naturalHeight},m.value.forEach(c=>{q(c)}),r({width:o,height:f,x:t,y:a})}catch(o){u(o)}})},K=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:n,canvasWidth:l,canvasHeight:s,originalWidth:r,originalHeight:u}=P.value;if(e<a||e>a+l||t<n||t>n+s)return null;const o=(e-a)/l,f=(t-n)/s;return{x:Math.round(o*r),y:Math.round(f*u)}},ge=e=>{const t=K(e);if(d.drawingMode==="delete"){const a=ee(t);a!==null&&z(a);return}d.drawingMode!=="none"&&(N.value=t,C.value=t,d.drawingMode==="rectangle"?W.value=!0:d.drawingMode==="polygon"?y.value.push(t):(d.drawingMode==="freestyle"||d.drawingMode==="freeform")&&(W.value=!0,g.value=[t]))},ye=e=>{if(se(e),d.drawingMode==="none"||d.drawingMode==="delete")return;const t=K(e);if(C.value=t,d.drawingMode==="rectangle"&&W.value)M(),ke();else if(d.drawingMode==="polygon"&&y.value.length>0)M(),De();else if((d.drawingMode==="freestyle"||d.drawingMode==="freeform")&&W.value){const a=g.value[g.value.length-1],n=Math.sqrt(Math.pow(t.x-a.x,2)+Math.pow(t.y-a.y,2)),l=d.freestyleSensitivity;n>l&&g.value.push(t),M(),Re()}},me=()=>{d.drawingMode==="rectangle"&&W.value?(W.value=!1,Me()&&M()):(d.drawingMode==="freestyle"||d.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=()=>{E.value=!1},we=()=>{d.drawingMode==="polygon"&&y.value.length>=2&&Q()},xe=e=>{e.preventDefault(),d.drawingMode==="polygon"&&y.value.length>=2&&Q()},Q=()=>{be()&&(y.value=[],M())},Me=()=>{const e=N.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)},n=R(a.x,a.y),l=R(a.x+a.width,a.y+a.height);if(!n||!l)return null;const s={x:n.x,y:n.y,width:l.x-n.x,height:l.y-n.y};return T("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:T("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,d.simplificationTolerance);console.log("Original path points:",g.value.length,"Simplified points:",e.length);const t=e.map(n=>R(n.x,n.y)).filter(n=>n!==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=d.drawingMode==="freeform"?"freeform":"freestyle";return T(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,n=(r,u,o,f,c)=>{let v=f,b=-1;for(let I=u+1;I<o;I++){const te=Pe(r[I],r[u],r[o]);te>v&&(b=I,v=te)}v>f&&(b-u>1&&n(r,u,b,f,c),c.push(r[b]),o-b>1&&n(r,b,o,f,c))},l=e.length-1,s=[e[0]];return n(e,0,l,a,s),s.push(e[l]),s},Pe=(e,t,a)=>{const n=a.x-t.x,l=a.y-t.y;let s,r;if(n!==0||l!==0){const u=((e.x-t.x)*n+(e.y-t.y)*l)/(n*n+l*l);u>1?(s=e.x-a.x,r=e.y-a.y):u>0?(s=e.x-(t.x+n*u),r=e.y-(t.y+l*u)):(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=N.value,t=C.value,a=Math.min(e.x,t.x),n=Math.min(e.y,t.y),l=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,n,l,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(),Z(i.value,g.value,!1),g.value.length>2&&i.value.closePath(),i.value.stroke(),i.value.restore())},Z=(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 l=1;l<t.length-1;l++){const s=t[l],r=t[l+1],u=(s.x+r.x)/2,o=(s.y+r.y)/2;e.quadraticCurveTo(s.x,s.y,u,o)}const n=t[t.length-1];if(e.lineTo(n.x,n.y),a&&t.length>2){const l=t[0],s=(n.x+l.x)/2,r=(n.y+l.y)/2;e.quadraticCurveTo(n.x,n.y,s,r)}},M=()=>{if(j(),k.value&&P.value){const{canvasX:e,canvasY:t,canvasWidth:a,canvasHeight:n}=P.value;i.value.drawImage(k.value,e,t,a,n)}m.value.forEach(e=>{q(e)})},Ce=()=>m.value,He=()=>{m.value=[],M()},ee=e=>{for(let t=m.value.length-1;t>=0;t--){const a=m.value[t];if(a.type==="rectangle"){const n=a.canvas;if(e.x>=n.x&&e.x<=n.x+n.width&&e.y>=n.y&&e.y<=n.y+n.height)return a.id}else if((a.type==="polygon"||a.type==="freestyle"||a.type==="freeform")&&Ie(e,a.canvas))return a.id}return null},Te=e=>m.value.find(t=>t.id===e),Ie=(e,t)=>{let a=!1;const n=e.x,l=e.y;for(let s=0,r=t.length-1;s<t.length;r=s++){const u=t[s].x,o=t[s].y,f=t[r].x,c=t[r].y;o>l!=c>l&&n<(f-u)*(l-o)/(c-o)+u&&(a=!a)}return a},z=e=>{const t=m.value.findIndex(a=>a.id===e);if(t>=0){const a=m.value.splice(t,1)[0];return M(),_("shape-removed",a),a}return null},Le=e=>{if(typeof e=="string")return z(e);if(e>=0&&e<m.value.length){const t=m.value[e];return z(t.id)}return null},_e=()=>{j(),m.value=[],k.value=null,P.value=null,_("canvas-reset")};h.watch(()=>d.pasteEnabled,e=>{U.value=e}),h.watch(()=>d.drawingMode,e=>{(e==="none"||e==="delete")&&(W.value=!1,y.value=[],g.value=[])});const Ee=()=>i.value,Xe=()=>D.value,Ye=()=>({width:p.value,height:w.value});return h.onMounted(()=>{ae(),window.addEventListener("resize",O),window.addEventListener("paste",J),$(),h.nextTick(()=>{G()})}),h.onUnmounted(()=>{window.removeEventListener("resize",O),window.removeEventListener("paste",J),X&&clearTimeout(X)}),Y({addImage:F,drawRectangle:re,drawPolygon:ce,clearCanvas:j,resetCanvas:_e,getContext:Ee,getCanvas:Xe,getCanvasSize:Ye,pasteImage:he,getImage:de,updateImage:ve,setImagePasteEnabled:fe,getDrawnShapes:Ce,clearDrawnShapes:He,removeShape:Le,removeShapeById:z,findShapeAtPosition:ee,findShapeById:Te,renderShape:q,storeShape:T,setMagnifierEnabled:e=>{e||(E.value=!1)}}),(e,t)=>(h.openBlock(),h.createElementBlock("div",{ref_key:"containerRef",ref:B,class:"ml-canvas-container"},[h.createElementVNode("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:h.normalizeStyle({cursor:oe.value})},null,44,ne),h.createElementVNode("div",{ref_key:"magnifierRef",ref:H,class:h.normalizeClass(["magnifier",{visible:E.value&&d.magnifierEnabled}])},[h.createElementVNode("canvas",{ref_key:"magnifierCanvasRef",ref:A,class:"magnifier-canvas"},null,512)],2)],512))}}}));