cover_image

Canvas从入门到实战

LLS-FE团队 流利说技术团队
2022年12月20日 10:00

1、什么是Canvas?

HTML5 提供Canvas API,其本质上是一个DOM元素,可以看成是浏览器提供一块画布供我们在上面渲染2D或者3D图形。由于3D绘制上下文(webgl)目前在很多浏览器上兼容性较差,所以我们一般用于绘制2D图形。


<canvas id="canvas"></canvas>


2、为什么使用Canvas?


Canvas是HTML5引入的标签,在此之前我们通常会使用SVG来绘制一些图形,那么两者之间有什么区别呢?SVG可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言XML描述的2D图形的语言,两者部分区别:


  • SVG 图像是使用各种元素创建的,这些元素分别应用于矢量图像的结构、绘制与布局;而Canvas本身并不描述图像,而是通过Javascript完成绘制;

  • 如上所述,SVG本身是DOM元素,每一个描述元素也是DOM元素,浏览器在进行渲染时需要进行大量计算以处理每一个元素;而在渲染Canvas的过程中,浏览器只需要渲染一张画布,其余的是通过Javascript引擎执行逻辑来绘制;

  • SVG(矢量图)不依赖分辨率,放大不会失真;而Canvas(位图)依赖分辨率,放大会失真;



由于Canvas是通过Javascript来完成绘制的,所以可控性很强,我们可以比较精确的控制图形渲染的每一帧;从另一方面来说,如果在高频率渲染中要处理过多的DOM元素就意味着性能一定不会太好,渲染速度会下降很多。Canvas的高性能能够保障复杂场景中图形的渲染效率,所以目前很多领域都会使用Canvas,例如动画、游戏图形、数据可视化、照片处理和实时视频处理等。



3、Canvas的基本使用

要使用Canvas,我们需要先获取Canvas元素的引用继而通过getContext()方法获取图形的绘制上下文。

const canvas = document.getElementById('canvas')const ctx = canvas.getContext('2d')


获取到图形绘制上下文后,我们就能使用CanvasRenderingContext2D接口上的绘图API了,接下来我们可以了解一些比较常规的使用。


3.1、画布属性:


  • width、height:画布的宽度以及高度,默认大小为300x150;

  • fillStyle:填充图形的样式,值可以是color string、CanvasGradient对象;

  • strokeStyle:轮廓图形的样式,值可以是color string、CanvasGradient对象;

  • lineWidth:绘制线条的宽度;

  • globalAlpha:画布的透明度,0-1的偏移值;

  • globalCompositeOperation:画布中新老图形重叠时的渲染方式,默认为source-over,新图形覆盖老图形;

  • ......

ctx.width = 300ctx.height = 300ctx.fillStyle = '#fff'ctx.strokeStyle = 'blue'ctx.lineWidth = 5ctx.globalAlpha = 0.3ctx.globalCompositeOperation = 'destination-out' // 新老图形重叠部分变透明......

3.2、绘制图形:


  • .fillRect(x,y,width,height):绘制一个填充的矩形,矩形左上角的坐标为(x,y),高宽分别为width、height;

  • .strokeRect(x,y,width,height):绘制一个矩形边框,矩形左上角的坐标为(x,y),高宽分别为width、height;

  • .clearRect(x,y,width,height):清除指定矩形区域,让清除部分完全透明;

ctx.fillStyle = 'red' ctx.fillRect(100,100,100,100)
ctx.strokeStyle = 'blue' ctx.strokeRect(200,200,100,100)
ctx.clearRect(125,125,50,50)ctx.strokeRect(130,130,40,40)


图片



3.3、绘制路径:


  • .beginPath():开始一段路径的绘制

  • .closePath():从起始点到当前点,结束路径的绘制,非必需

  • .fill():根据路径生成填充图形;

  • .stroke():通过路径生成轮廓图形;

  • .moveTo(x,y):声明一段路径的起始点;

  • .lineTo(x,y):绘制一条从当前坐标到(x,y)的线;

ctx.beginPath()ctx.moveTo(50,50)ctx.lineTo(100,100)ctx.lineTo(100,0)ctx.fill()
ctx.beginPath()ctx.moveTo(110,100)ctx.lineTo(150,100)ctx.lineTo(150,200)ctx.lineTo(110,200)ctx.closePath() // 轮廓图形不会根据从当前坐标到起始坐标生成轮廓,所以需要闭合路径ctx.stroke()

图片


3.4、绘制圆弧:


  • .arc(x,y,radius,startAngle,endAngle,anticlockwise):画一个以(x,y)为圆心的以 radius 为半径的圆弧(圆),从 startAngle 开始到 endAngle 结束,按照 anticlockwise 给定的方向(默认为顺时针,false)来生成;

  • arcTo(x1,y1,x2,y2,radius):根据给定的两条切线中的一组切点坐标生成半径为radius的圆弧;

   图片


注意:arc函数中的角度的单位是弧度而不是度,弧度=(Math.PI/180)*度

// 圆左上部分ctx.beginPath()ctx.arc(100,100,50,Math.PI,Math.PI*3/2,false)ctx.strokeStyle = '#ff6700'ctx.stroke()
// 圆右上部分ctx.beginPath()ctx.arc(100,100,50,Math.PI*3/2,0,false)ctx.strokeStyle = '#6700ff'ctx.stroke()
// 圆右下部分ctx.beginPath()ctx.arc(100,100,50,0,Math.PI/2,false)ctx.strokeStyle = '#00FFFF'ctx.stroke()
// 圆左下部分ctx.beginPath()ctx.arc(100,100,50,Math.PI/2,Math.PI,false)ctx.strokeStyle = '#8B008B'ctx.stroke()
// 两条切线的交点坐标为(0,0)ctx.beginPath()ctx.moveTo(100,0)ctx.arcTo(0,0,0,100,100)ctx.fillStyle = 'blue'ctx.fill()

图片


3.5、渐变对象:


  • .createLinearGradient(x1, y1, x2, y2):创建一个沿参数坐标指定的直线的渐变,开始坐标为(x1,y1),结束坐标为(x2,y2);

  • .createRadialGradient(x1, y1, r1, x2, y2, r2):创建根据参数确定两个圆的坐标的放射性渐变,开始圆形圆心为(x1,y1),半径为r1;结束圆形圆心为(x2,y2),半径为r2;

创建好渐变对象之后,可以通过渐变对象上的.addColorStop(offset,color)为每一个渐变阶段填充颜色,offset为0-1的偏移值。

const gradient = ctx.createLinearGradient(50, 50, 250, 50)gradient.addColorStop(0, 'blue')gradient.addColorStop(0.5, 'green')gradient.addColorStop(1, 'red')ctx.fillStyle = gradientctx.fillRect(0, 0, 300, 90)
const radialGradient = ctx.createRadialGradient(200,200,100,200,200,50);radialGradient.addColorStop(0,"yellow");radialGradient.addColorStop(1,"green");ctx.fillStyle = radialGradient;ctx.fillRect(100,100,200,200);

图片



3.6、像素操作:


  • .drawImage(image,x,y,width,height):image可以是image对象、canvas元素、video元素;

  • .getImageData(x,y,width,height):获取坐标为(x,y)一定区域内图像的像素数据;

const div = document.querySelector('div')let mousedown = false;
function getRandom() { return Math.round(255 * Math.random());}
function getColor() { return `rgb(${getRandom()},${getRandom()},${getRandom()})`;}
const gradient = ctx.createLinearGradient(0, 0, 300, 300);gradient.addColorStop(0, getColor());gradient.addColorStop(0.6, getColor());gradient.addColorStop(1, getColor());
function clear() { ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height);}
ctx.beginPath();ctx.fillStyle = gradient;ctx.fillRect(0, 0, 300, 300);
function selector(x = 150, y = 150) { clear(); ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.strokeStyle = "#fff"; ctx.stroke(); const { data } = ctx.getImageData(x, y, 1, 1); // 获取(x,y)点对应的imageData const color = `rgba(${data[0]},${data[1]},${data[2]},${data[3] / 255})` div.innerText = `color: ${color}`; div.style.backgroundColor = color}
function handleSelector(e) { const x = e.offsetX; const y = e.offsetY; selector(x, y);}
canvas.addEventListener("mousedown", (e) => { mousedown = true; handleSelector(e)});
canvas.addEventListener("mouseup", () => { mousedown = false;});
canvas.addEventListener("mousemove", (e) => { if (mousedown) { handleSelector(e) }});
selector();

图片



3.7、画布状态:


  • .save():将当前画布的状态推入到栈中,例如fillStyle、2D转换等;

  • .restore():将栈顶元素弹出,恢复上一次推入栈中画布的状态;

当我们需要通过空间转换来绘制图形时,保存与恢复画布的状态是很关键的,因为我们是在同一块画布上绘制图形,而变换都是基于画布的,这与我们平时使用到的CSS 2D转换截然不同,所以我们在下一步绘制时要确认此时画布的状态是否是我们的理想状态。

ctx.save() // 保存画布初始状态ctx.translate(100,100) // 将画布原点转移至(100,100)ctx.fillStyle = 'red'ctx.fillRect(0,0,50,50)
ctx.restore() // 恢复画布状态,此时画布原点为(0,0)ctx.fillStyle = 'blue'ctx.fillRect(0,0,50,50)

图片

3.8、几何变化:


  • .translate(x,y):画布默认的原点是(0,0),此方法可以切换原点到(x,y)而不需要手动更改绘制图形的坐标;

  • .rotate(angle):将画布旋转一定的角度,angle单位为弧度;

  • .scale(sx,sy):sx为水平方向的缩放比例,sy为竖直方向的缩放比例;

  • .transform(a,b,c,d,e,f):依次为水平缩放、垂直倾斜、水平倾斜、垂直缩放、水平移动、垂直移动;


const colors = ['red','orange','yellow','green','blue','purple'];ctx.translate(150,150)    for(let i = 0; i < 6; i++) {  ctx.beginPath()  ctx.fillStyle = colors[i]  ctx.moveTo(0,0)  ctx.lineTo(100,0)  ctx.lineTo(100,50)  ctx.rotate(Math.PI/3)  ctx.fill()}

图片



4、综合实战


const p = Math.PI;
function clock() { const date = new Date(); const hour = date.getHours() const s = date.getSeconds();  const m = date.getMinutes(); const h = !!(hour % 12) ? hour % 12 : 12; ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save(); // 保存画布初始状态
ctx.translate(150, 150); ctx.rotate(-p / 2);
// 轮廓 ctx.beginPath(); ctx.lineWidth = 5; ctx.strokeStyle = "#76b2ff"; ctx.arc(0, 0, 80, 0, p * 2); ctx.stroke();
// 圆心 ctx.beginPath(); ctx.arc(0, 0, 2, 0, p * 2); ctx.fill();
// 分针、秒针刻度 for (let i = 0; i < 60; i++) { ctx.beginPath(); ctx.rotate(p / 30); ctx.moveTo(75, 0); ctx.lineWidth = 4; ctx.strokeStyle = "#89f086"; ctx.lineTo(80, 0); ctx.stroke(); }
// 时针刻度 for (let i = 0; i < 12; i++) { ctx.beginPath() ctx.rotate(p / 6) ctx.moveTo(70, 0) ctx.lineTo(80, 0) ctx.stroke() }
ctx.save(); // 保存画布变换之后的状态
// 秒针 ctx.beginPath(); ctx.rotate(s * (p / 30)); ctx.lineWidth = 2 ctx.strokeStyle = '#ff6700' ctx.moveTo(0, 0); ctx.lineTo(80, 0); ctx.stroke();
// 恢复之前的状态再保存,时针、分针、秒针都是基于原点以及画布方向变换后绘制 ctx.restore(); ctx.save();
// 分针 ctx.beginPath(); ctx.rotate(m * (p / 30)); ctx.lineWidth = 3; ctx.strokeStyle = '#6700ff' ctx.moveTo(0, 0); ctx.lineTo(70, 0); ctx.stroke();
ctx.restore();
// 时针 ctx.beginPath(); ctx.rotate(h * (p / 6)); ctx.lineWidth = 4; ctx.moveTo(0, 0); ctx.lineTo(60, 0); ctx.stroke();
ctx.restore(); // 恢复画布最初状态
document.querySelector('div').innerText = `Now:${h} : ${m} : ${s} ${hour > 12 ? 'pm' : 'am'}`
window.requestAnimationFrame(clock);}clock();


图片



5、小结


随着互联网的高速发展,用户对页面的视觉和交互有着越来越高的要求,传统的web开发无法得到满足,利用Canvas强大的绘图能力,可以让网页显示的内容更加的丰富多彩,也能给用户带来更好的视觉体验。

继续滑动看下一个
流利说技术团队
向上滑动看下一个