canvasflow
Version:
Draw objects, Move them, Control them.
1 lines • 13.9 kB
JavaScript
let errorCodes=new Map([[100,"Missing props. Objects require an 'id' property."],[101,"Invalid argument. Argument must be an object."],[102,"Invalid argument. Argument must be an array of objects."],[103,"Invalid canvas. No canvas captured."],[104,"Invalid argument. Argument must be a string."],[105,"Invalid argument. Argument must be a function."],[106,"Invalid Id. No object found with that Id."],[107,"Missing props. Custom objects require a 'draw' property."],[108,"Invalid props. 'draw' must be a function."],[109,"Missing props. Objects require a 'type' property."],[110,"Missing props. Image objects require a 'url' property."],[111,"Updating props declined. 'id' cannot be updated."],[112,"Updating props declined. 'type' cannot be updated."],[113,"Invalid initialize props. 'defaultValues' must be an object."],[114,"Missing props. Text objects require a 'text' property."],[115,"Missing props. Path objects require a 'path' property."],[116,"Missing props. Line objects require 'from' and 'to' properties."],[117,"Invalid props. 'scale' must be 0 or a positive number."],[118,"Invalid props. 'isChunk' can only be used with circle, path, and rectangle.",],[119,"Missing props. Polygon objects require a 'points' property."],]);export default class t{objects=new Map;images=new Map;defaultValues=new Map([["x",0],["y",0],["width",0],["height",0],["fill","black"],["stroke",{fill:"black",width:0}],["font",{family:"sans-serif",size:10}],["borderRadius",0],["rotation",0],["opacity",1],["zIndex",0],["translate",{x:0,y:0}],["scale",1],["isChunk",!1],["points",[]],["blurness","0px"],["brightness",1],["contrast",1],["grayscale",0],["hueRotate",0],["invert",0],["saturate",1],["sepia",0],["dropShadow",{}],]);canvas;ctx;touch=null;constructor(t,e){if(!(t instanceof HTMLElement)&&"string"!=typeof t)throw Error(errorCodes.get(103));if(e&&e.defaultValues){if("object"!=typeof e.defaultValues)throw Error(errorCodes.get(113));Object.entries(e.defaultValues).forEach(([t,e])=>{this.defaultValues.set(t,e)})}this.canvas="string"==typeof t?document.querySelector(t):t,this.ctx=this.canvas.getContext("2d")}setObject(t){return new Promise((e,s)=>{if("object"!=typeof t)return s(Error(errorCodes.get(101)));let{id:r,...i}=t;if(null==r)return s(Error(errorCodes.get(100)));if("path"===i.type&&i.path){let{x:o,y:a,width:h,height:l}=getPathProps(i.path);i.x||(i.x=o),i.y||(i.y=a),i.width||(i.width=h),i.height||(i.height=l)}if(this.objects.set(r,i),"image"===i.type){if(!i.url)return s(Error(errorCodes.get(110)));let n=new Image;n.src=i.url,n.onload=()=>{this.images.set(r,n);let s=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);s.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})}),e(t)},n.onerror=s}else{let c=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);c.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})}),e(t)}})}setObjects(t){return new Promise((e,s)=>{if(!Array.isArray(t)||t.some(t=>"object"!=typeof t||!t))return s(Error(errorCodes.get(102)));let r=[];if(t.forEach(t=>{let{id:e,...i}=t;if(null==e)return s(Error(errorCodes.get(100)));if("path"===i.type&&i.path){let{x:o,y:a,width:h,height:l}=getPathProps(i.path);i.x||(i.x=o),i.y||(i.y=a),i.width||(i.width=h),i.height||(i.height=l)}if("image"===i.type){if(!i.url)return s(Error(errorCodes.get(110)));let n=new Image;n.src=i.url;let c=new Promise(t=>{n.onload=()=>{this.images.set(e,n),t()},n.onerror=s});r.push(c)}this.objects.set(e,i)}),0===r.length){let i=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);i.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})}),e(t)}else Promise.all(r).then(()=>{let s=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);s.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})}),e(t)}).catch(s)})}updateObject(t,e){return new Promise((s,r)=>{if(null==t)return r(Error(errorCodes.get(100)));if("object"!=typeof e)return r(Error(errorCodes.get(101)));let i=this.objects.get(t);if(!i)return r(Error(errorCodes.get(106)));let o=!1;if(Object.entries(e).forEach(([t,e])=>{if("id"===t)return r(Error(errorCodes.get(111)));if("type"===t)return r(Error(errorCodes.get(112)));if("url"===t&&(o=!0),"path"===t&&"path"===i.type){let{x:s,y:a,width:h,height:l}=getPathProps(e);i.x||(i.x=s),i.y||(i.y=a),i.width||(i.width=h),i.height||(i.height=l)}i[t]=e}),"image"===i.type&&o){if(!i.url)return r(Error(errorCodes.get(110)));let a=new Image;a.src=i.url,a.onload=()=>{this.images.set(t,a);let e=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);e.forEach(({id:t,width:e,height:s})=>{this.objects.set(t,{...this.objects.get(t),width:e,height:s})}),s(i)},a.onerror=r}else{let h=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);h.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})}),s(i)}})}getObject(t){if(null==t)throw Error(errorCodes.get(100));return this.objects.get(t)}getObjects(){return sortByZIndex(this.objects.entries(),this.defaultValues).map(([t,e])=>({id:t,...e}))}getObjectsByClassName(t){return sortByZIndex(this.objects.entries(),this.defaultValues).filter(([,e])=>e.classes.split(" ").includes(t)).map(([t,e])=>({id:t,...e}))}deleteObject(t){this.objects.delete(t),this.images.delete(t);let e=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);e.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})})}isObjectExists(t){return this.objects.has(t)}moveObject(t,e,s,r){if(null==t)throw Error(errorCodes.get(100));let i=this.objects.get(t);if(!i)throw Error(errorCodes.get(106));["line","path","polygon"].includes(i.type)?i.translate?"relative"===r?(i.translate.x+=e,i.translate.y+=s):(i.translate.x=e,i.translate.y=s):i.translate={x:e,y:s}:"relative"===r?(i.x=(i.x??0)+e,i.y=(i.y??0)+s):(i.x=e,i.y=s);let o=drawCanvas(this.canvas,this.objects,this.ctx,this.images,this.defaultValues);o.forEach(({id:t,x:e,y:s,width:r,height:i})=>{let o=this.objects.get(t),a=o.x??e,h=o.y??s,l=o.width??r,n=o.height??i;this.objects.set(t,{...o,x:a,y:h,width:l,height:n})})}clearCanvas(){if(!this.canvas)throw Error(errorCodes.get(103));this.objects.clear(),this.ctx.setTransform(1,0,0,1,0,0),this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}on(t,e){if("string"!=typeof t)throw Error(errorCodes.get(104));if("function"!=typeof e)throw Error(errorCodes.get(105));if(!this.canvas)throw Error(errorCodes.get(103));let s=s=>{s.objects=sortByZIndex(this.objects.entries(),this.defaultValues).filter(([,{x:e,y:r,from:i,to:o,translate:a,width:h,height:l,type:n,path:c,points:g,text:d,scale:u,rotation:f,stroke:p},])=>{let $,_;if(t.startsWith("touch")){if("touchstart"===t&&null==this.touch&&(this.touch=s.touches[0].identifier),"touchend"===t){if(this.touch!==s.changedTouches[0].identifier)return!1;$=s.changedTouches[0].clientX,_=s.changedTouches[0].clientY,this.touch=null}if(this.touch!==s.changedTouches[0].identifier)return!1;$=s.touches[0].clientX,_=s.touches[0].clientY}else $=s.offsetX,_=s.offsetY;e=(e??0)*(u??1),r=(r??0)*(u??1),h=(h??0)*(u??1),l=(l??0)*(u??1),a&&(e=(a.x??0)+(e??0),r=(a.y??0)+(r??0));let y=new Path2D;if("circle"===n)y.arc(e,r,h/2,0,2*Math.PI);else if("path"===n){if(!(c??this.defaultValues.get("path")))throw Error(errorCodes.get(115));let{x,y:w,width:b,height:m}=getPathProps(c??this.defaultValues.get("path"));a&&(x=(a.x??0)+(x??0),w=(a.y??0)+(w??0)),y.rect(x,w,b,m)}else if("polygon"===n){let v=g??this.defaultValues.get("points");if(!v)throw Error(errorCodes.get(119));a&&(v=v.map(t=>({x:t.x+(a.x??0),y:t.y+(a.y??0)}))),y.moveTo(v[0].x,v[0].y);for(let j=1;j<v.length;j++)y.lineTo(v[j].x,v[j].y);y.lineTo(v[0].x,v[0].y)}else if("line"===n){if(!i?.x||!i?.y||!o?.x||!o?.y)throw Error(errorCodes.get(116));let C=Math.min(i.x,o.x)+e,k=Math.max(i.x,o.x)+e,P=Math.min(i.y,o.y)+r,T=Math.max(i.y,o.y)+r;y.rect(C,P,k-C,T-P)}else if("text"===n){if(null==d)throw Error(errorCodes.get(114));let I=y.font?.size??this.defaultValues.get("font").size,V=y.font?.family||this.defaultValues.get("font").family;this.ctx.textBaseline="top",this.ctx.font=`${I}px ${V}`;let E=this.ctx.measureText(d);y.rect(e,r,E.width,E.actualBoundingBoxAscent+E.actualBoundingBoxDescent)}else y.rect(e,r,h,l);return this.ctx.isPointInPath(y,$,_)||this.ctx.isPointInStroke(y,$,_)}).map(([t,e])=>({id:t,...e})),e(s)};return this.canvas.addEventListener(t,s),()=>{this.canvas.removeEventListener(t,s)}}};function drawCanvas(t,e,s,r,i){if(!t)throw Error(errorCodes.get(103));s.setTransform(1,0,0,1,0,0),s.filter="none",s.clearRect(0,0,t.width,t.height);let o=[];return sortByZIndex(e.entries(),i).forEach(([e,a])=>{let h=a.type||i.get("type");if(!h)throw Error(errorCodes.get(109));let l=a.scale??i.get("scale");if(l<=0)throw Error(errorCodes.get(117));let n=a.x??i.get("x"),c=a.y??i.get("y"),g=a.text??i.get("text"),d=a.isChunk??i.get("isChunk");if(s.setTransform(1,0,0,1,0,0),d){if(!["circle","path","rectangle"].includes(h))throw Error(errorCodes.get(118));let u=a.width??i.get("width"),f=a.height??i.get("height");if(s.scale(l,l),"circle"===h)s.globalCompositeOperation="destination-out",s.beginPath(),s.ellipse(n,c,u/2,f/2,0,0,2*Math.PI),s.fill(),s.globalCompositeOperation="source-over";else if("path"===h){s.save();let p=a.path;if(!p)throw Error(errorCodes.get(115));s.clip(new Path2D(p)),s.clearRect(0,0,t.width,t.height),s.restore();let{x:$,y:_,width:y,height:x}=getPathProps(p);o.push({id:e,width:y,height:x,x:$,y:_})}else"rectangle"===h&&s.clearRect(n,c,u,f);return}s.setTransform(1,0,0,1,0,0),s.scale(l,l),s.fillStyle=a.fill||i.get("fill"),s.strokeStyle=a.stroke?.fill||i.get("stroke").fill,s.lineWidth=a.stroke?.width??i.get("stroke").width,s.globalAlpha=a.opacity??i.get("opacity");let w=a.blurness??i.get("blurness"),b=a.brightness??i.get("brightness"),m=a.contrast??i.get("contrast"),v=a.grayscale??i.get("grayscale"),j=a.hueRotate??i.get("hueRotate"),C=a.invert??i.get("invert"),k=a.saturate??i.get("saturate"),P=a.sepia??i.get("sepia"),T=a.dropShadow??i.get("dropShadow"),I=[];if(w&&I.push(`blur(${a.blurness})`),1!==b&&I.push(`brightness(${100*a.brightness}%)`),1!==m&&I.push(`contrast(${100*a.contrast}%)`),0!==v&&I.push(`grayscale(${100*a.grayscale}%)`),0!==j&&I.push(`hue-rotate(${a.hueRotate}deg)`),0!==C&&I.push(`invert(${100*a.invert}%)`),1!==k&&I.push(`saturate(${100*a.saturate}%)`),0!==P&&I.push(`sepia(${100*a.sepia}%)`),T){let{x:V,y:E,blur:B,spread:O,color:z}=T;V&&E&&B&&O&&z&&I.push(`drop-shadow(${V}px ${E}px ${B}px ${O}px ${z})`)}if(s.filter=I.join(" "),"text"===h){if(null==g)throw Error(errorCodes.get(114));let R=a.font?.size??i.get("font").size,q=a.font?.family||i.get("font").family;s.textBaseline="top",s.font=`${R}px ${q}`,s.fillText(g,n,c),a.stroke&&a.stroke.width&&a.stroke.fill&&s.strokeText(g,n,c)}else{let S=a.width??i.get("width"),A=a.height??i.get("height"),M=a.borderRadius??i.get("borderRadius"),N=a.translate??i.get("translate"),Z=a.rotation??i.get("rotation");if(Z&&(s.translate(n+S/2,c+A/2),s.rotate(Z*Math.PI/180),s.translate(-n-S/2,-c-A/2)),N&&(N.x||N.y)&&s.translate(N.x,N.y),"custom"===h){let{draw:L,...X}=a;if(!L)throw Error(errorCodes.get(107));if("function"!=typeof L)throw Error(errorCodes.get(108));L(s,t,X)}else if("image"===h){let Y=r.get(e);Y&&s.drawImage(Y,n,c,S,A)}else if("path"===h){s.beginPath();let U=a.path;if(!U)throw Error(errorCodes.get(115));let W=new Path2D(U);s.fill(W),a.stroke&&a.stroke.width&&a.stroke.fill&&s.stroke(W),s.closePath();let{x:D,y:F,width:G,height:H}=getPathProps(U);o.push({id:e,width:G,height:H,x:D,y:F})}else if("polygon"===h){s.beginPath();let J=a.points??i.get("points");if(!J)throw Error(errorCodes.get(119));s.moveTo(J[0].x,J[0].y);for(let K=1;K<J.length;K++)s.lineTo(J[K].x,J[K].y);s.lineTo(J[0].x,J[0].y),s.fill(),a.stroke&&a.stroke.width&&a.stroke.fill&&s.stroke(),s.closePath()}else if("rectangle"===h)s.beginPath(),s.moveTo(n+M,c),s.lineTo(n+S-M,c),s.arcTo(n+S,c,n+S,c+M,M),s.lineTo(n+S,c+A-M),s.arcTo(n+S,c+A,n+S-M,c+A,M),s.lineTo(n+M,c+A),s.arcTo(n,c+A,n,c+A-M,M),s.lineTo(n,c+M),s.arcTo(n,c,n+M,c,M),s.fill(),a.stroke&&a.stroke.width&&a.stroke.fill&&s.stroke(),s.closePath();else if("triangle"===h)s.beginPath(),s.moveTo(n+M,c),s.arcTo(n+S,c,n+S,c+A,M),s.arcTo(n+S,c+A,n,c+A,M),s.lineTo(n,c+M),s.arcTo(n,c,n+M,c,M),s.fill(),a.stroke&&a.stroke.width&&a.stroke.fill&&s.stroke(),s.closePath();else if("line"===h){s.beginPath();let Q=a.from??i.get("from"),tt=a.to??i.get("to");if(!Q?.x||!Q?.y||!tt?.x||!tt?.y)throw Error(errorCodes.get(116));s.moveTo(Q.x,Q.y),s.lineTo(tt.x,tt.y),a.stroke&&a.stroke.width&&a.stroke.fill&&s.stroke(),s.closePath()}else"circle"===h&&(s.beginPath(),s.ellipse(n,c,S/2,A/2,0,0,2*Math.PI),s.fill(),a.stroke&&a.stroke.width&&a.stroke.fill&&s.stroke(),s.closePath())}}),s.setTransform(1,0,0,1,0,0),o}function sortByZIndex(t,e){return[...t].sort(([,t],[,s])=>{let r=t.zIndex??e.get("zIndex"),i=s.zIndex??e.get("zIndex");return void 0===r&&void 0===i?0:void 0===r&&i>0?-1:void 0===i&&r>0?1:(isNaN(r)&&(r=1/0),isNaN(i)&&(i=1/0),r-i)})}function getPathProps(t){let e=document.createElementNS("http://www.w3.org/2000/svg","svg"),s=document.createElementNS("http://www.w3.org/2000/svg","path");s.setAttribute("d",t),e.appendChild(s),document.body.appendChild(e);let r=s.getBBox();return document.body.removeChild(e),{width:r.width,height:r.height,x:r.x,y:r.y}}