Canvas 2D绘图指南:从基础到实战 Canvas 2Dcanvas是html中的一个标签用width和height属性来设置画布的宽高然后通过getContext方法来获取画笔对象。canvas idcanvas width500 height500/canvas通过getContext方法获取画笔对象canvas.getContext(2d)canvas 对象的获取方法(原生Html中)canvasEl document.getElementById(canvas)或者在vue3中一般采用 ElementRef 的方式获取:canvas refcanvasRef width500 height500/canvasconstcanvasRefrefHTMLCanvasElement|null(null);letcanvasEl!:HTMLCanvasElement;onMounted((){constcanvasElcanvasRef.value;});除此之外还有一种DOM结构的获取方式(Vue3.5以上版本)canvas refcanvasRef width500 height500/canvas// useTemplateRef 中的参数要与元素中的ref属性一致// 但DOM元素的变量名可以不用与ref一致例如当前命名为canvas// ref获取DOM结构需使用与ref一致的变量名// useTemplateRef 可自动推导DOM类型constcanvasuseTemplateRef(canvasRef);Canvas的基本操作Canvas(画布)的坐标原点在左上角,向右边为x轴,向下边为y轴绘制直线 / 折线 / 矩形绘制直线需要设置起点和终点然后调用lineTo方法绘制直线。constcanvasElrefHTMLCanvasElement|null(null);varctxcanvasEl.getContext(2d);// 开始绘制ctx.beginPath();// 移动画笔ctx.moveTo(0,0);// 绘制直线ctx.lineTo(500,500);// 轮廓ctx.stroke();// 绘制折线ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(250,500);ctx.lineTo(500,500);ctx.stroke();// 使用fill()方法填充ctx.fill();// 使用closePath()方法闭合路径ctx.closePath();// 绘制矩形ctx.beginPath();// 参数依次为(x,y,width,height)ctx.rect(0,0,100,100);ctx.stroke();// 上方方法等价于直接使用strokeRect()方法绘制矩形ctx.strokeRect(0,0,100,100);// 实心矩阵ctx.fillRect(0,0,100,100);// 清除矩形ctx.clearRect(0,0,50,50);绘制曲线绘制曲线需要设置曲线的起点和终点然后设置曲线的曲线控制点。圆弧ctx.arc(x,y,r,startAngle,endAngle,anticlockwise)x y 圆的坐标r 圆的半径startAngle 起点弧度endAngle 终点弧度anticlockwise 默认false 为顺时针true为逆时针一次贝塞尔曲线ctx.quadraticCurveTo(cpx,cpy,x,y)cpx,cpy 控制点的坐标x,y 终点的坐标二次贝塞尔曲线ctx.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)cp1x,cp1y 控制点1的坐标cp2x,cp2y 控制点2的坐标x,y 终点的坐标绘制文本ctx.fillText(text,x,y)ctx.strokeText(text,x,y)text 文本内容x,y 文本的坐标绘制图片ctx.drawImage(img,sx,sy,sWidth,sHeight,dx,dty,dWidth,dHeight)img 图片对象sx,sy 图片的起始坐标sWidth,sHeight 图片的宽高dx,dy 图片的绘制坐标dWidth,dHeight 图片的绘制宽高绘制样式ctx.fillStylectx.strokeStyle其中颜色的表示方式有如下几种ctx.fillStyle redctx.fillStyle rgb(255,0,0)ctx.fillStyle rgba(255,0,0,0.5)ctx.fillStyle #ff0000默认fillStyle / strokeStyle 为黑色线宽ctx.lineWidth默认为1线结尾样式ctx.lineCap默认 buttbutt 默认以方形结尾round 圆形结尾加圆帽子square 方形结尾加帽子线结合样式ctx.lineJoin默认mitermiter 默认以尖角连接bevel 斜角连接round 圆角连接虚线ctx.setLineDash(dashArray)其中dashArray为数组数组中的前一个元素为虚线长度第二个元素为虚线间隔长度ctx.setLineDash([5,10])样式存档机制存档当前样式设置ctx.save()恢复最新存档样式ctx.restore()ctx.fillStyleblue;ctx.lineCapround;// 保存的是 blue, roundctx.save();ctx.fillStylered;ctx.lineCapsquare;ctx.fillRect(0,0,100,100);// 恢复的是 blue, roundctx.restore();变形ctx.translate(x,y)移动坐标原点ctx.rotate(angle)旋转画板坐标系angle为弧度制ctx.scale(x,y)缩放画板坐标系在变形操作之前可以使用save()方法保存当前的坐标系在变形操作之后可以使用restore()方法恢复Demo1画板 Nuxt4script setup langtsletpaintingfalse;letstartPoint:{x:number,y:number}|undefined;letctx:CanvasRenderingContext2D;// 画布上下文// 画布DOMconstcanvasuseTemplateRef(canvasRef);// 画板Card DOMconstcanvasCarduseTemplateRef(cardRef);// 画笔线宽constlineWidthref(1);// 开始绘画constdrawStart(e:MouseEvent){// 获取鼠标关于元素位置的坐标let{offsetX:x,offsetY:y}e;startPoint{x,y};paintingtrue;// 设置画笔线宽ctx.lineWidthlineWidth.value;};// 绘画持续constdrawMove(e:MouseEvent){let{offsetX:x,offsetY:y}e;letnewPoint{x,y};if(painting){ctx.beginPath();if(!startPoint)return;ctx.moveTo(startPoint.x,startPoint.y);ctx.lineTo(newPoint.x,newPoint.y);ctx.stroke();ctx.closePath();startPointnewPoint;}};// 停止绘画constdrawStop(e:MouseEvent){paintingfalse;startPointundefined;};// 切换画板constswitchCard(){if(!canvasCard.value)return;constcanvasCardElcanvasCard.value;canvasCardEl.style.visibilitygetComputedStyle(canvasCardEl).visibilityhidden?visible:hidden;}// 保存图片constsaveCanvas(isWhiteBG:booleanfalse){if(!canvas.value)return;constcanvasElcanvas.value;letexportCanvasElcanvasEl;// 创建一个临时的白色背景 canvas 用于保存的图片背景是白色if(isWhiteBG){consttempCanvasdocument.createElement(canvas);tempCanvas.widthcanvasEl.width;tempCanvas.heightcanvasEl.height;consttempCtxtempCanvas.getContext(2d);if(!tempCtx)return;tempCtx.fillStylewhite;tempCtx.fillRect(0,0,tempCanvas.width,tempCanvas.height);// 将原图片绘制到临时canvas上tempCtx.drawImage(canvasEl,0,0);exportCanvasEltempCanvas;}exportCanvasEl.toBlob((blob){if(!blob)return;// 创建一个URL对象consturlURL.createObjectURL(blob);// 定义一个a标签并且自启动点击constadocument.createElement(a);a.hrefurl;a.downloadcanvas;a.click();// 释放内存URL.revokeObjectURL(url);},image/jpeg)}onMounted((){// 获取画布DOMif(!canvas.value)return;constcanvasElcanvas.value;// 获取画布上下文constcontentcanvasEl.getContext(2d);// 缓存画布上下文if(!content)return;ctxcontent;// 同步缓冲区 实际显示大小canvasEl.widthcanvasEl.clientWidth;canvasEl.heightcanvasEl.clientHeight;// 如果窗口大小变化也随之变化constobservernewResizeObserver((){canvasEl.widthcanvasEl.clientWidth;canvasEl.heightcanvasEl.clientHeight;})observer.observe(canvasEl);// 设置画笔逻辑监听canvasEl.addEventListener(pointerdown,drawStart);canvasEl.addEventListener(pointermove,drawMove);canvasEl.addEventListener(pointerup,drawStop);})/scripttemplatedivclassHomedivclassmenudivclassbuttonclickswitchCard画板/divdivclassbuttonidbtn_resetclickctx.reset重置/divdivclassbuttonidbtn_saveclicksaveCanvas(true)保存/div/divdiv refcardRefclasscarddivclasstopdivclassbuttonstylebackground: #FE574A;/divdivclassbuttonstylebackground: #FBB124;/divdivclassbuttonstylebackground: #2AC037;/divdivclassLineWidthRangepLineWidth/pinput typerangemin1max10v-model.numberlineWidthp{{lineWidth}}/p/div/divcanvas refcanvasRefidcanvas/canvas/div/div/templatestyle langlessscopedmenu_bg:#fafafa80;// 背景图片 该网页会随机给出一张背景图home_bg:url(https://t.alcy.cc/pc)no-repeat center center/cover;.Home{width:100%;height:100vh;background:home_bg;// 防止元素溢出出现滚动条overflow:hidden;}.menu{width:100%;display:flex;background-color:menu_bg;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);flex-direction:row;gap:25px;padding-left:60px;padding-top:5px;padding-bottom:10px;.button{cursor:pointer;:nth-child(1)::before{content:;}:nth-child(2)::before{content:;}:nth-child(3)::before{content:;}}}.card{width:600px;height:800px;display:flex;flex-direction:column;background-color:white;border-radius:10px;box-shadow:10px-10px 20pxrgba(0,0,0,0.5);margin:0auto;margin-top:50px;visibility:visible;.top{display:flex;gap:10px;padding-left:10px;.button{width:20px;height:20px;border-radius:50%;margin-top:10px}.LineWidthRange{display:flex;margin-top:8px;padding-left:10px;font-weight:bold;}}canvas{flex:1;width:100%;// 禁止触摸事件 移动端适配touch-action:none;}}/styleDemo2时钟 Nuxt4script setup langtsconstcanvasuseTemplateRef(canvasRef)constanimate(){constnownewDate();constsecnow.getSeconds()constminnow.getMinutes()lethournow.getHours()hourhour%12if(!canvas.value)return;constctxcanvas.value.getContext(2d)if(!ctx)return;ctx.save();ctx.clearRect(0,0,canvas.value.width,canvas.value.height);ctx.translate(canvas.value.width/2,canvas.value.height/2);ctx.rotate(-Math.PI/2);ctx.strokeStyleblack;ctx.lineCapround;constdrawTick(count:number,len:number,width:number){ctx.save();ctx.lineWidthwidth;for(leti0;icount;i){ctx.beginPath();ctx.rotate(Math.PI*2/count);ctx.moveTo(120-len,0);ctx.lineTo(120,0);ctx.stroke();}ctx.restore();}// 绘制时钟刻度drawTick(12,20,5);drawTick(60,10,3);// 时针ctx.save();ctx.rotate(hour*Math.PI/6min*Math.PI/360sec*Math.PI/21600)ctx.lineWidth14;ctx.beginPath();ctx.moveTo(-20,0);ctx.lineTo(80,0);ctx.stroke();ctx.restore();// 分针ctx.save();ctx.rotate(min*Math.PI/30sec*Math.PI/1800)ctx.lineWidth10;ctx.beginPath();ctx.moveTo(-28,0);ctx.lineTo(105,0);ctx.stroke();ctx.restore();// 秒针ctx.save();ctx.rotate(sec*Math.PI/30)ctx.lineWidth6;ctx.strokeStyle#D40000ctx.fillStyle#D40000ctx.beginPath();ctx.moveTo(-30,0);ctx.lineTo(110,0);ctx.stroke();ctx.arc(0,0,10,0,Math.PI*2,true);ctx.fill();ctx.restore();ctx.restore();requestAnimationFrame(animate)}onMounted((){if(!canvas.value)return;letcanvasElcanvas.value;// 同步缓冲区 实际显示大小canvasEl.widthcanvasEl.clientWidth;canvasEl.heightcanvasEl.clientHeight;constobservernewResizeObserver((){canvasEl.widthcanvasEl.clientWidth;canvasEl.heightcanvasEl.clientHeight;})observer.observe(canvasEl);// 开启动画requestAnimationFrame(animate);})/scripttemplatedivclassClockcanvas refcanvasRef/canvas/div/templatestyle langlessscoped.Clock{width:100%;height:100vh;display:flex;canvas{flex:1;width:100%;}}/style