canvas 基础知识
什么是canvas
canvas 是 HTML5 新定义的一个标签,通过 Javascript 在网页上绘制图像。
<canvas>
只是一个容器,表示一个画布,本身没有绘制图像的功能。所有的绘制工作必须在 JavaScript 内部完成。
canvas
默认为 300*150 的无边框画布,可以通过 width height 属性控制其大小。
💡 不能通过css样式控制width height,这样会使画布及内容按照 300*150 比例缩放。
<canvas id="myCanvas" width="200" height="100"></canvas>
document.getElementById 方法获取HTML
获取 <canvas>
元素的引用。
const canvas = document.getElementById('canvas');
getContext 获取上下文
getContext(contextType)
方法返回canvas
的上下文,如果上下文没有定义则返回 [null
]。在同一个canvas上以相同的 contextType
多次调用此方法只会返回同一个上下文。
const ctx = canvas.getContext('2d')
contextType
为上下文类型:
2d
用于创建二维渲染上下文CanvasRenderingContext2D
用于绘制形状,文本,图像和其他对象。webgl
webgl2
用于创建三维渲染上下文
canvas 绘制
canvas 创建图形的方式有两种,fill 和 stroke。
- fill 绘制的是填充图形;
- stroke 绘制的是描边图形,实际是通过moveTo() lineTo() 方法绘制的路径。
图形样式
包括颜色和样式(渐变、图案、阴影)。默认 #000
(黑色).
// 图形内部的颜色和样式。
ctx.fillStyle = style
// 图形边线的颜色和样式
ctx.strokeStyle = style
渐变和图案
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'green');
gradient.addColorStop(.5, 'white');
gradient.addColorStop(1, 'green');
ctx.strokeStyle = gradient;
ctx.lineWidth = 100;
// ctx.lineCap = lineCap;
ctx.beginPath();
ctx.moveTo(100, 200);
ctx.lineTo(100, 0);
ctx.stroke();
ctx.closePath();
var img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = function() {
var pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(200, 0, 100,200);
};
实现思路:
- 💡
const gradient = ctx.createLinearGradient()
创建一个沿参数坐标指定的直线的渐变。- 💡 使用
gradient.addColorStop
增加渐变断点- 💡
const pattern = ctx.createPattern()
设置图片样式- 💡
ctx.fillStyle = gradient
设置填充渐变;ctx.strokeStyle = gradient
设置边框渐变;- 💡
ctx.fillStyle = pattern
设置图片填充。
ctx.createLinearGradient(x0, y0, x1, y1)
创建一个沿参数坐标指定的直线(x0, y0) 到 (x1, y1)的渐变。
CanvasGradient.addColorStop(offset, color)
方法根据指定的偏移(0-1)和颜色定义一个新的渐变断点。
ctx.createPattern(image, repetition)
使用指定的图像创建样式,通过repetition参数在指定的方向上重复元图像,repetition
包括 repeat
repeat-x
repeat-y
no-repeat
。
阴影
ctx.shadowBlur
描述模糊效果。 默认 0
ctx.shadowColor
阴影的颜色。
ctx.shadowOffsetX
阴影水平方向的偏移量。 默认 0
ctx.shadowOffsetY
阴影垂直方向的偏移量。 默认 0
线型和路径
线样式
ctx.lineWidth
设置线宽,默认1.0。0、 负数、 Infinity 和 NaN 会被忽略。
ctx.lineCap
线末端的类型。 允许的值: butt
线段末端以方形结束(默认)。round
线段末端以圆形结束。square
线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域。
ctx.lineJoin
定义两线相交拐点的类型。允许的值:round
弧形, bevel
三角, miter
菱形(默认)。
ctx.setLineDash()
使用虚线模式填充线段,它使用一组值来指定描述模式的线和间隙的交替长度。如果数组元素的数量是奇数, 数组的元素会被复制并重复。[5, 10, 15] 👉 [5, 10, 15, 5, 10, 15]
ctx.lineDashOffset
设置虚线偏移量的属性。
ctx.getLineDash()
获取当前线段样式的数组,数组包含一组数量为偶数的非负数数字。
操作路径
ctx.beginPath()
清空子路径列表开始一个新路径的方法。当你想创建一个新的路径时,调用此方法。
ctx.closePath()
笔触返回到当前子路径起始点的方法。它尝试从当前点到起始点绘制一条直线,用于绘制一个封闭的图形。 如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。和beginPath
几乎没有什么关系,想重新开始一条路径只能通过调用 beginPath()
。
ctx.moveTo(x, y)
将一个新的子路径的起始点移动到(x,y)坐标的方法。
ctx.lineTo(x, y)
使用直线连接子路径的最后的点到x,y坐标。
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
添加一个3次贝赛尔曲线路径。该方法需要三个点。 第一个点(cp1x, cp1y)、第二个点(cp2x, cp2y)是控制点,第三个点( x, y)是结束点。起始点是当前路径的最后一个点,绘制贝赛尔曲线前,可以通过调用 moveTo()
进行修改。
ctx.quadraticCurveTo(cpx, cpy, x, y)
添加二次贝塞尔曲线路径的方法。它需要2个点。 第一个点是控制点,第二个点是终点。
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)
绘制圆弧路径的方法。 圆弧路径的圆心在 (x, y) 位置,半径为r ,根据 anticlockwise(默认为为false顺时针)指定的方向从 startAngle 开始绘制,到 endAngle 结束,endAngle, anticlockwise单位为弧度。
ctx.arcTo(x1, y1, x2, y2, radius)
根据控制点和半径绘制圆弧路径。根据当前描点与给定的控制点1(x1, y1)连接的直线,和控制点1(x1, y1)与控制点2(x2, y2)连接的直线,作为使用指定半径的圆的切线,画出两条切线之间的弧线路径。
ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
绘制椭圆形,目前还在实验阶段。
ctx.rect(x, y, width, height)
创建矩形路径的方法,矩形的起点位置是 (x, y),尺寸为 width 和 height。矩形的4个点通过直线连接,为闭合的路径。
缺少 beginPath 引发的问题
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
ctx.strokeStyle = 'black';
ctx.lineWidth =2;
ctx.setLineDash([5, 10, 15]);
ctx.beginPath();
ctx.moveTo(0, 120); // 1
ctx.lineTo(400, 120); // 2
ctx.stroke();
// ctx.beginPath();
ctx.strokeStyle = 'red';
ctx.moveTo(0, 220); // 3
ctx.lineTo(400, 220); // 4
ctx.stroke();
未调用beginPath的两条线都是红色是为什么呢?
canvas在绘制时(stroke/fill),都会以上一次beginPath之后的所有路径为基础绘制,也就是第一次stroke()绘制了第一条黑色的线(1、2),第二次绘制两条红色的线(1、2、3、4)覆盖第一条黑色的线。(可以试试不同粗细的线看1-2画了两次的效果)
如果不调用beginPath() 那么绘制的图形就都是同一个路径,即使他们可能不连续。
绘制路径
ctx.fill()
使用当前的样式填充子路径。调用fill时,closePath是没有用的,因为有没有调用closePath,canvas会自动把没有封闭的路径首尾相连,之后进行填充。
ctx.stroke()
使用当前的样式描边子路径。
ctx.scrollPathIntoView()
将当前或给定的路径滚动到窗口的方法,该功能还在实验中。类似于 Element.scrollIntoView()
。
ctx.clip()
将当前创建的路径设置为当前剪切路径的方法,调用clip()后,绘制的所有信息只会出现在剪切路径内部。
ctx.isPointInPath(x, y)
用于判断在当前路径中是否包含检测点的方法。
ctx.isPointInStroke(x, y)
判断检测点是否在路径的描边线上。
矩形
// 绘制填充矩形,矩形的起点在 (x, y) 位置,矩形的尺寸是 width 和 height
ctx.fillRect(x, y, width, height)
// 使用当前的笔触样式,描绘一个起点在 (x, y) 位置,尺寸是 width 和 height 的矩形
ctx.strokeRect(x, y, width, height)
// 设置指定矩形区域内(以起点在 (x, y) 位置,范围是 (width, height) )所有像素变成透明,并擦除之前绘制的所有内容。
ctx.clearRect(x, y, width, height)
文本
文本样式
ctx.font
设置字体样式的属性。默认字体是 10px sans-serif
ctx.textAlign
文本对齐设置。 允许的值: start
(默认), end
, left
, right
或 center
。
ctx.textBaseline
基线对齐设置。 允许的值: top
, hanging
, middle
, alphabetic
(默认), ideographic
, bottom
。
ctx.direction
设置文字方向,允许的值: ltr, rtl, inherit (默认)。
绘制文本
ctx.fillText()
绘制填充文字。
ctx.strokeText()
绘制描边文字。
ctx.measureText()
获取被检测的文本对象信息,例如文本宽度width等。
ctx.lineWidth = 1;
ctx.font = "30px";
var text = ctx.measureText('text');
console.log(text.width);
绘制图像
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
绘制指定图片。
sx
图片的矩形裁剪选择框的左上角 X 轴坐标sy
图片的矩形裁剪选择框的左上角 Y 轴坐标sWidth
图片的矩形裁剪选择框的选择框的宽度sHeight
图片的矩形裁剪选择框的选择框的高度dx
图片左上角在目标canvas上 X 轴坐标dy
图片左上角在目标canvas上 Y 轴坐标dWidth
图片在目标canvas上绘制的宽度dHeight
图片在目标canvas上绘制的高度
变换
ctx.rotate(angle)
以(0,0)为圆心旋转一个顺时针旋转角度,单位弧度。
ctx.scale(x, y)
根据 x 水平方向和 y 垂直方向的缩放因子进行缩放变换。
ctx.scale(-1, 1)
水平翻转上下文,使用ctx.scale(1, -1)
垂直翻转上下文
ctx.translate(x, y)
将原点按水平方向距离x、垂直方向距离y进行平移变换
ctx.transform(a, b, c, d, e, f)
多次叠加当前的变换矩阵。a水平缩放;b垂直倾斜;c水平倾斜;d垂直缩放;e水平移动;f垂直移动。
ctx.setTransform(a, b, c, d, e, f)
重新设置(覆盖)当前的变换并调用变换的方法。参数和transform相同,不同点就在setTransform覆盖当前变换重新设置;translate方法不会覆盖当前的变换矩阵,会多次叠加变换。
ctx.resetTransform()
重新设置当前的变换。该方法还在实验中。
合成
ctx.globalAlpha
在合成到 canvas 之前,设置图形和图像透明度的值。默认 1.0
(不透明)。
ctx.globalCompositeOperation
设置要在绘制新形状时应用的合成操作的类型。
source-over
这是默认设置,并在现有画布上下文之上绘制新图形。source-in
新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。source-out
在不与现有画布内容重叠的地方绘制新图形。source-atop
新图形只在与现有画布内容重叠的地方绘制。destination-over
在现有的画布内容后面绘制新的图形。destination-in
现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的。destination-out
现有内容保持在新图形不重叠的地方。(刮刮乐可以用到这种模式)destination-atop
现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。lighter
两个重叠图形的颜色是通过颜色值相加来确定的。copy
只显示新图形。xor
重叠和正常绘制,之外的其他地方是透明的。
绘制不规则图形
多边形
const canvasBg = document.getElementById('myBg') as HTMLCanvasElement;
const ctxBg = canvasBg.getContext('2d') as CanvasRenderingContext2D;
ctxBg.globalCompositeOperation = 'destination-over';
ctxBg.fillStyle = '#2F5BEA';
ctxBg.beginPath();
ctxBg.lineTo(1920, 0);
ctxBg.lineTo(1920, 410);
ctxBg.lineTo(820, 500);
ctxBg.lineTo(0, 350);
ctxBg.closePath();
ctxBg.fill();
ctxBg.fillStyle = '#c2cdf9';
ctxBg.beginPath();
ctxBg.lineTo(1920, 0);
ctxBg.lineTo(1920, 425);
ctxBg.lineTo(520, 500);
ctxBg.lineTo(0, 410);
ctxBg.closePath();
ctxBg.fill();
var base64Img = canvasBg.toDataURL("image/png");
console.log(base64Img);
扇形
const graphCanvas = document.getElementById('graphCanvas') as HTMLCanvasElement;
const ctxGraph = graphCanvas.getContext('2d') as CanvasRenderingContext2D;
ctxGraph.lineWidth = 0.4;
ctxGraph.save();
// 方法1,两条边都从圆心开始画
ctxGraph.translate(30,30);
ctxGraph.save();
ctxGraph.arc(0,0,30,0,45*Math.PI/180);
ctxGraph.restore();
ctxGraph.moveTo(0,0);
ctxGraph.lineTo(30,0);
ctxGraph.rotate(45*Math.PI/180);
ctxGraph.moveTo(0,0);
ctxGraph.lineTo(30,0);
ctxGraph.stroke();
ctxGraph.restore();
ctxGraph.save();
// // 方法2,利用笔触位置绘制两条边
ctxGraph.beginPath();
ctxGraph.translate(90,30);
ctxGraph.arc(0,0,30,45*Math.PI/180, 0, true);
ctxGraph.lineTo(0,0);
ctxGraph.rotate(45*Math.PI/180);
ctxGraph.lineTo(30,0);
ctxGraph.stroke();
ctxGraph.restore();
ctxGraph.save();
// 方法3,利用closePath
ctxGraph.beginPath();
ctxGraph.translate(150,30);
ctxGraph.moveTo(0,0);
ctxGraph.arc(0,0,30,0,45*Math.PI/180);
ctxGraph.closePath();
ctxGraph.stroke();
圆角矩形
实现思路:
- arcTo(x1, y1, x2, y2, r) 绘制出和 当前点——(x1, y1)、(x1, y1) ——(x2, y2) 两条线想切的圆弧。
- 绘制出和矩形两条边相切的圆弧。
![]()
const drawCircleRect = (x: number, y: number, width: number, height: number, radio: number) => {
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
ctx.beginPath();
ctx.moveTo(x, radio + y);
ctx.arcTo(x, height + y, width + x, height + y, radio);
ctx.arcTo(width + x, height + y, width + x, y, radio);
ctx.arcTo(width + x, y, x, y, radio);
ctx.arcTo(x, y, x, radio + y, radio);
ctx.fill();
}
实际应用
钟表
实现思路:
- 绘制表盘:原点移动到表盘中心,绘制圆形表盘
- 绘制表针:(0,10) (0, -40)的直线指向12点,根据当前时间计算旋转角度
- 绘制刻度:小时为等份12分,分钟则是每小时再等分5份
- 动起来:利用save和restore 每秒重新绘制所有图形
<template>
<canvas id="clockCanvas" width="400" height="200"></canvas>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
let canvas = null as HTMLCanvasElement, ctx = null as CanvasRenderingContext2D;
const oneCircle = 2 * Math.PI;
const clockSize = 65;
onMounted(() => {
canvas = document.getElementById('clockCanvas') as HTMLCanvasElement;
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
drowClock();
setInterval(drowClock, 1000);
})
const drowClock = () => {
ctx.save();
ctx.clearRect(0, 0, 400, 200);
ctx.fillStyle = 'gray';
ctx.font = "18px serif";
ctx.fillText('⏰ 时钟', 10, 30);
// 将坐标移动到表盘圆形
ctx.translate(250, 100);
drawOutline(); // 绘制轮廓
drawHands(); // 绘制表针
drawScale(); // 绘制刻度
ctx.restore();
};
const drawOutline = () => {
// 先画个圆形表盘
ctx.beginPath();
ctx.arc(0, 0, clockSize, 0, oneCircle);
ctx.stroke();
// 再画一个圆形的表芯
ctx.beginPath(); // 重新画一个圆形,如果不调用该方法会将两个圆形链起来
ctx.arc(0, 0, 5, 0, oneCircle);
ctx.stroke();
};
const getCurrentTimeDegs = () => {
const time = new Date();
const hour = time.getHours() % 12;
const min = time.getMinutes();
const second = time.getSeconds();
return {
hour: oneCircle / 12 * hour + oneCircle / 12 / 60 * min, // 秒针转动的角度忽略
min: oneCircle / 60 * min + oneCircle / 60 / 60 * second,
second: oneCircle / 60 * second
};
};
const drawHands = () => {
const { hour, min, second } = getCurrentTimeDegs();
ctx.save();
// 时针
ctx.rotate(hour);
ctx.beginPath();
// 绘制指向 12 的支线
ctx.moveTo(0, 10);
ctx.lineTo(0, -40);
ctx.lineWidth = 8;
ctx.strokeStyle = 'blue';
ctx.stroke();
ctx.restore();
ctx.save();
// 分针
ctx.rotate(min);
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(0, -50);
ctx.lineWidth = 5;
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.restore();
ctx.save();
// 秒针
ctx.rotate(second);
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(0, -55);
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.stroke();
ctx.restore();
};
const drawScale = () => {
// 绘制刻度 小时、分钟
for (let index = 0; index < 12; index++) {
ctx.save();
ctx.beginPath();
ctx.rotate(oneCircle / 12 * index);
ctx.moveTo(0, -clockSize);
ctx.lineTo(0, -clockSize + 8);
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.stroke();
ctx.restore();
for (let i = 1; i < 5; i++) {
ctx.save();
ctx.beginPath();
ctx.rotate(oneCircle / 12 * index + oneCircle / 12 / 5 * i);
ctx.moveTo(0, -clockSize);
ctx.lineTo(0, -clockSize + 6);
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
}
}
};
</script>
刮刮乐
实现思路:
- 奖品作为背景,上层覆盖一层灰色canvas,遮盖住奖品。
- 给canvas增加事件 mousedown、mousemove、mouseup,鼠标经过的位置fill圆形。
- 设置ctx.globalCompositeOperation = 'destination-out',使鼠标经过的位置变为透明。
<div class="ticket-wrap">
<canvas id="ticketCanvas" width="400" height="200" @mousedown="start" @mousemove="draw" @mouseup="stop"></canvas>
</div>
<script lang="ts" setup>
import { onMounted } from "vue";
let canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D;
onMounted(() => {
canvas = document.getElementById('ticketCanvas') as HTMLCanvasElement;
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
drawBg('谢谢惠顾');
ctx.fillStyle = 'gray';
ctx.fillRect(0,0,400,300);
ctx.fillStyle = "#fff";
ctx.font = "48px serif";
ctx.fillText('刮刮乐', 130, 130);
})
let isDraw = false;
const drawBg = (text: string) => {
const canvasBg = document.createElement('canvas') as HTMLCanvasElement;
canvasBg.width = 400;
canvasBg.height = 200;
const ctxBg = canvasBg.getContext('2d') as CanvasRenderingContext2D;
ctxBg.font = "40px serif";
const textMeasure = ctx.measureText(text);
ctxBg.fillText(text, 130, 130);
const base64Img = canvasBg.toDataURL('image/png');
canvas.style.background = `url(${base64Img})`;
}
const start = (event: any) => {
isDraw = true;
}
const draw = (event: any) => {
if (!isDraw) return;
ctx.beginPath();
const x = event.offsetX;
const y = event.offsetY;
// ctx.fillStyle = 'red';
ctx.globalCompositeOperation = 'destination-out';
ctx.arc(x, y, 10, 0, 2*Math.PI);
ctx.fill();
}
const stop = (event: any) => {
isDraw = false;
}
</script>
<style lang="less" scoped>
.ticket-wrap {
width: 400px;
height: 200px;
position: relative;
#ticketCanvas {
position: absolute;
top: 0;
left: 0;
z-index: 10;
}
.text {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 50px;
font-weight: bold;
}
}
</style>
电子签
实现思路
- 画线: mousedown 时记录moveTo坐标,mousemove 获取lineTo坐标并更新moveTo坐标。
- 撤销一步 每次mousedown时都要通过 getImageData 记录一下当前状态存放在imgStack中,撤销一步时 putImageData 撤销会上一步的状态。清空 使用clearRect 清空画布,并且清空 imgStack。
- 保存 使用 canvas.toDataURL 将当前canvas状态保存为base64。
<div class="sign-wrap">
<div class="operate">
<el-button size="mini" type="primary" @click="handleClear">清空</el-button>
<el-button size="mini" type="primary" @click="handleReset">撤销一步</el-button>
<el-button size="mini" type="primary" @click="handleConfirm">保存</el-button>
</div>
<canvas id="signCanvas" width="400" height="200" @mousedown="start" @mousemove="draw" @mouseup="stop"></canvas>
</div>
<script lang="ts" setup>
import { onMounted } from "vue";
let canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D;
onMounted(() => {
canvas = document.getElementById('signCanvas') as HTMLCanvasElement;
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
})
let isDraw = false;
let lastX = 0, lastY = 0, WIDTH = 400, HEIGHT = 200;
const imgStack = [] as any[];
const start = (event: any) => {
isDraw = true;
lastX = event.offsetX;
lastY = event.offsetY;
let imgData = ctx.getImageData(0, 0, WIDTH, HEIGHT);
imgStack.push(imgData);
}
const draw = (event: any) => {
if (!isDraw) return;
ctx.beginPath();
const x = event.offsetX;
const y = event.offsetY;
// 画图
// ctx.globalCompositeOperation = 'destination-out';
drawLine(x, y);
}
const stop = (event: any) => {
isDraw = false;
}
const drawLine = (x: number, y: number) => {
ctx.beginPath();
ctx.lineWidth = 2; //设置线宽状态
ctx.strokeStyle = 'red'; //设置线的颜色状态
ctx.lineCap = 'round'
ctx.lineJoin = "round";
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
// 每次移动都要更新坐标位置
lastX = x;
lastY = y;
}
const handleClear = () => {
imgStack.length = 0;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
}
const handleReset = () => {
if (imgStack.length === 0) return;
ctx.putImageData(imgStack.pop(), 0, 0);
}
const handleConfirm = () => {
const base64Img = canvas.toDataURL('image/png');
const img = document.createElement('img');
img.src = base64Img;
downloadUrl(base64Img, '签名.png');
}
</script>
饼图
实现思路:
- 画一个扇形:一条弧线,通过closePath闭合扇形。(可参考前面画扇形的方法3)
- 空心扇形:在内部一个小的扇形,通过设置 globalCompositeOperation = 'destination-out' 擦除内部形成空心。
- 依次画出整个圆环。
- 入场动画:利用 window.requestAnimationFrame 没帧画一个小角度(speed),直到画完 cancelAnimationFrame 停止动画。
const dataArr = [{
data: 0.2,
color: "#5470c6"
}, {
data: 0.2,
color: "#91cb75"
}, {
data: 0.2,
color: "#fac858"
}, {
data: 0.2,
color: "#ee6465"
}, {
data: 0.2,
color: "#73c0de"
}];
let canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D;
let cx: number, cy : number;
const iR = 60, oR = 90; // 内圆和外圆半径
onMounted(() => {
canvas = document.getElementById('pieCanvas') as HTMLCanvasElement;
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
// 设置圆心位置
cx = canvas.width / 2;
cy = canvas.height / 2;
ctx.translate(cx, cy); // 将画布原点移动到圆心
// renderAnimation(); // 带有入场动画
render(); // 无入场动画
})
// 画整个圆环
const render = () => {
let startAngle = 0, angle = 0, index = 0;
for (let index = 0; index < dataArr.length; index++) {
const angle = dataArr[index].data * 2 * Math.PI;
drawSector(startAngle, startAngle + angle, dataArr[index].color);
startAngle += angle;
}
}
// 增加入场动画
const renderAnimation = () => {
const speed = 10 / 180 * Math.PI; // 动画速度
let startAngle = 0, angle = 0, index = 0;
const renderGraph = () => {
const timmer = window.requestAnimationFrame(renderGraph);
angle += speed;
if (angle >= dataArr[index].data * 2 * Math.PI) {
angle = dataArr[index].data * 2 * Math.PI;
}
drawSector(startAngle, startAngle + angle, dataArr[index].color);
if (angle >= dataArr[index].data * 2 * Math.PI) {
startAngle += dataArr[index].data * 2 * Math.PI;
angle = 0;
index ++;
if (index === dataArr.length) cancelAnimationFrame(timmer);
}
}
renderGraph();
}
// 画一个空心扇形
const drawSector = (startAngle: number, endAngle: number, color: string) => {
ctx.save();
// 画外圆
getGraph(startAngle, endAngle, oR);
ctx.fillStyle = color
ctx.fill();
ctx.globalCompositeOperation = 'destination-out';
// 画内圆
getGraph(startAngle, endAngle, iR);
ctx.fill();
// 内全画完后会留下一个边覆盖不全
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
};
// 画一个扇形,默认坐标原点为圆心
const getGraph = (startAngle: number, endAngle: number, radius: number) => {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, startAngle, endAngle); // 弧形
ctx.closePath(); // 回到原心
};