Canvas 2D
canvas是html中的一个标签,用width和height属性来设置画布的宽高,然后通过getContext方法来获取画笔对象。
<canvas id='canvas' width="500" height="500"></canvas>
通过getContext方法获取画笔对象canvas.getContext('2d')
canvas 对象的获取方法(原生Html中):canvasEl = document.getElementById('canvas')
或者在vue3中一般采用 ElementRef 的方式获取:<canvas ref="canvasRef" width="500" height="500"></canvas>
constcanvasRef=ref<HTMLCanvasElement|null>(null);letcanvasEl!:HTMLCanvasElement;onMounted(()=>{constcanvasEl=canvasRef.value;});除此之外,还有一种DOM结构的获取方式(Vue3.5以上版本)<canvas ref="canvasRef" width="500" height="500"></canvas>
// useTemplateRef 中的参数要与元素中的ref属性一致// 但DOM元素的变量名可以不用与ref一致,例如当前命名为canvas// ref获取DOM结构,需使用与ref一致的变量名// useTemplateRef 可自动推导DOM类型constcanvas=useTemplateRef("canvasRef");Canvas的基本操作
Canvas(画布)的坐标原点在左上角,向右边为x轴,向下边为y轴
绘制直线 / 折线 / 矩形
绘制直线需要设置起点和终点,然后调用lineTo方法绘制直线。
constcanvasEl=ref<HTMLCanvasElement|null>(null);varctx=canvasEl.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 = 'red'ctx.fillStyle = 'rgb(255,0,0)'ctx.fillStyle = 'rgba(255,0,0,0.5)'ctx.fillStyle = '#ff0000'
默认fillStyle / strokeStyle 为黑色
线宽ctx.lineWidth,默认为1
线结尾样式ctx.lineCap,默认 butt
- butt 默认,以方形结尾
- round 圆形结尾,加圆帽子
- square 方形结尾,加帽子
线结合样式ctx.lineJoin,默认miter
- miter 默认,以尖角连接
- bevel 斜角连接
- round 圆角连接
虚线ctx.setLineDash(dashArray)
其中dashArray为数组,数组中的前一个元素为虚线长度,第二个元素为虚线间隔长度ctx.setLineDash([5,10])
样式存档机制
存档当前样式设置ctx.save()
恢复最新存档样式ctx.restore()
ctx.fillStyle="blue";ctx.lineCap="round";// 保存的是 blue, roundctx.save();ctx.fillStyle="red";ctx.lineCap="square";ctx.fillRect(0,0,100,100);// 恢复的是 blue, roundctx.restore();变形ctx.translate(x,y)移动坐标原点ctx.rotate(angle)旋转画板坐标系,angle为弧度制ctx.scale(x,y)缩放画板坐标系
在变形操作之前可以使用save()方法保存当前的坐标系,在变形操作之后可以使用restore()方法恢复
Demo1:画板 Nuxt4
<script setup lang="ts">letpainting=false;letstartPoint:{x:number,y:number}|undefined;letctx:CanvasRenderingContext2D;// 画布上下文// 画布DOMconstcanvas=useTemplateRef('canvasRef');// 画板Card DOMconstcanvasCard=useTemplateRef('cardRef');// 画笔线宽constlineWidth=ref(1);// 开始绘画constdrawStart=(e:MouseEvent)=>{// 获取鼠标关于元素位置的坐标let{offsetX:x,offsetY:y}=e;startPoint={x,y};painting=true;// 设置画笔线宽ctx.lineWidth=lineWidth.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();startPoint=newPoint;}};// 停止绘画constdrawStop=(e:MouseEvent)=>{painting=false;startPoint=undefined;};// 切换画板constswitchCard=()=>{if(!canvasCard.value)return;constcanvasCardEl=canvasCard.value;canvasCardEl.style.visibility=getComputedStyle(canvasCardEl).visibility==='hidden'?'visible':'hidden';}// 保存图片constsaveCanvas=(isWhiteBG:boolean=false)=>{if(!canvas.value)return;constcanvasEl=canvas.value;letexportCanvasEl=canvasEl;// 创建一个临时的白色背景 canvas 用于保存的图片背景是白色if(isWhiteBG){consttempCanvas=document.createElement('canvas');tempCanvas.width=canvasEl.width;tempCanvas.height=canvasEl.height;consttempCtx=tempCanvas.getContext('2d');if(!tempCtx)return;tempCtx.fillStyle='white';tempCtx.fillRect(0,0,tempCanvas.width,tempCanvas.height);// 将原图片绘制到临时canvas上tempCtx.drawImage(canvasEl,0,0);exportCanvasEl=tempCanvas;}exportCanvasEl.toBlob((blob)=>{if(!blob)return;// 创建一个URL对象consturl=URL.createObjectURL(blob);// 定义一个a标签并且自启动点击consta=document.createElement('a');a.href=url;a.download='canvas';a.click();// 释放内存URL.revokeObjectURL(url);},"image/jpeg")}onMounted(()=>{// 获取画布DOMif(!canvas.value)return;constcanvasEl=canvas.value;// 获取画布上下文constcontent=canvasEl.getContext('2d');// 缓存画布上下文if(!content)return;ctx=content;// 同步缓冲区 = 实际显示大小canvasEl.width=canvasEl.clientWidth;canvasEl.height=canvasEl.clientHeight;// 如果窗口大小变化也随之变化constobserver=newResizeObserver(()=>{canvasEl.width=canvasEl.clientWidth;canvasEl.height=canvasEl.clientHeight;})observer.observe(canvasEl);// 设置画笔逻辑监听canvasEl.addEventListener('pointerdown',drawStart);canvasEl.addEventListener('pointermove',drawMove);canvasEl.addEventListener('pointerup',drawStop);})</script><template><divclass="Home"><divclass="menu"><divclass="button"@click="switchCard">画板</div><divclass="button"id="btn_reset"@click="ctx.reset">重置</div><divclass="button"id="btn_save"@click="saveCanvas(true)">保存</div></div><div ref="cardRef"class="card"><divclass="top"><divclass="button"style="background: #FE574A;"></div><divclass="button"style="background: #FBB124;"></div><divclass="button"style="background: #2AC037;"></div><divclass="LineWidthRange"><p>LineWidth</p><input type="range"min="1"max="10"v-model.number="lineWidth"><p>{{lineWidth}}</p></div></div><canvas ref="canvasRef"id="canvas"></canvas></div></div></template><style lang="less"scoped>@menu_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;}}</style>Demo2:时钟 Nuxt4
<script setup lang="ts">constcanvas=useTemplateRef('canvasRef')constanimate=()=>{constnow=newDate();constsec=now.getSeconds()constmin=now.getMinutes()lethour=now.getHours()hour=hour%12if(!canvas.value)return;constctx=canvas.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.strokeStyle='black';ctx.lineCap='round';constdrawTick=(count:number,len:number,width:number)=>{ctx.save();ctx.lineWidth=width;for(leti=0;i<count;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/6+min*Math.PI/360+sec*Math.PI/21600)ctx.lineWidth=14;ctx.beginPath();ctx.moveTo(-20,0);ctx.lineTo(80,0);ctx.stroke();ctx.restore();// 分针ctx.save();ctx.rotate(min*Math.PI/30+sec*Math.PI/1800)ctx.lineWidth=10;ctx.beginPath();ctx.moveTo(-28,0);ctx.lineTo(105,0);ctx.stroke();ctx.restore();// 秒针ctx.save();ctx.rotate(sec*Math.PI/30)ctx.lineWidth=6;ctx.strokeStyle='#D40000'ctx.fillStyle='#D40000'ctx.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;letcanvasEl=canvas.value;// 同步缓冲区 = 实际显示大小canvasEl.width=canvasEl.clientWidth;canvasEl.height=canvasEl.clientHeight;constobserver=newResizeObserver(()=>{canvasEl.width=canvasEl.clientWidth;canvasEl.height=canvasEl.clientHeight;})observer.observe(canvasEl);// 开启动画requestAnimationFrame(animate);})</script><template><divclass="Clock"><canvas ref="canvasRef"></canvas></div></template><style lang="less"scoped>.Clock{width:100%;height:100vh;display:flex;canvas{flex:1;width:100%;}}</style>