高级Canvas绘图

前端开发
2018年12月15日
716

高级Canvas绘图

绘制图像

绘制上下文提供了drawImage()方法,用于在画面上绘制图片。使用这个方法很简单,调用它的时候传入相应的图片对象及起点坐标即可:

js
ctx.drawImage(img, 10, 10)

虽然,在调用drawImage()之前,需要准备好图片对象。HTML5为此提供了三个方案:

  1. 使用createImageData()一个像素一个像素地创建图像。
  2. 使用网页中已有的<img>元素
html
<img id="arrow_left" src="arrow_left.png">
js
var img = document.getElementById('arrow_left'); ctx.drawImage(img, 10, 10);
  1. 在代码中创建一个图片对象,然后把一个外部图片加载进来。这个方案有一个缺点,即必须先等待图片加载完毕,然后才能把图片对象传递给drawImage()使用。
js
// 创建图片对象 var img = new Image(); // 加载图片文件 img.src = 'maze.png'; img.onload = function () { ctx.drawImage(img, 0, 0); }

裁剪、切割和伸缩图片

drawImage()接受一些可选参数,可以影响在画面上绘制图片的方式。比如,如果想改变图片的大小,可以添加宽度和高度:

js
ctx.drawImage(img, 10, 10, 30, 30);

如果想裁剪掉一部分图片,可以再为drawImage()传入4个参数,这4个参数从图片对象参数后面开始:

js
ctx.drawImage(img, source_x, source_y, source_width, source_height, x, y, width, height);

绘制文本

首先,在绘制文本之前要设置绘图上下文的font属性。这个属性的值是一个字符串,与设置CSS的font属性时使用的“多合一”的值相同。

js
ctx.font = '20px Arial'; // 或者 ctx.font = '20px Verdana,sans-serif'; // 或者,实现加粗效果,要把它放在字符串开头 ctx.font = 'bold 20px Arial';

设置好字体后,就可以调用fillText()绘制文本内容了。

js
ctx.textBaseline = 'top'; ctx.fillStyle = 'black'; ctx.fillText('这里是一行文本内容!', 10, 10);

注意,fillText()每次只能绘制一行文本,如果要绘制多行文本,那只能多次调用。

除了fillText(),还有另一个绘制文本的方法:strokeText()。这个方法用于绘制文本的轮廓,轮廓的颜色取自strokeStyle属性,而轮廓的宽度取自lineWidth属性

js
ctx.font = 'bold 40px Verdana,sans-serif'; ctx.lineWidth = 1; ctx.strokeStyle = 'red'; ctx.StrokeText('Hello Canvas', 20, 50);

使用strokeText()时,文本的中部是空白。

阴影与填充

添加阴影

为绘制的任何内容添加阴影是Canvas最便利的功能。

属性 说明
shadowColor 设置阴影颜色
shadowBlur 设置阴影的模糊程度
shadowOffsetX、shadowOffsetY 设置阴影相对于内容的位置
js
// 绘制矩形阴影 ctx.rect(20, 20, 200, 100); ctx.fillStyle = '#8ed6ff'; ctx.shadowColor = '#bbb'; ctx.shadowBlur = 20; ctx.shadowOffseX = 15; ctx.shadowOffseY = 15; ctx.fill(); // 绘制星形阴影 ctx.shadowOffsetX = 10; ctx.shadowOffsetY = 10; ctx.shadowBlur = 4; img = document.getElementById('star'); ctx.drawImage(img, 250, 30); ctx.textBaseline = 'top'; ctx.font = 'bold 20px Arial'; // 绘制三行文本的阴影 ctx.shadowBlur = 3; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; ctx.fillStyle = 'steelblue'; ctx.fillText('这是第一行文本', 10, 175); ctx.shadowBlur = 5; ctx.shadowOffsetX = 20; ctx.shadowOffsetY = 20; ctx.fillStyle = 'steelblue'; ctx.fillText('这是第二行文本', 10, 225); ctx.shadowBlur = 15; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowColor = '#000'; ctx.fillStyle = 'steelblue'; ctx.fillText('这是第三行文本', 10, 300);

填充图案

要实现用图案填充,首先要选择一张小图片,而且能够前后拼接在一起覆盖一块大区域。我们可以利用绘图上下文的createPattern()方法创建一个图案对象,可以选择图案是水平(repeat-x)、垂直(repeat-y),还是在两个方向(repeat)重复:

js
var img = document.getElementById('brickTile'); var pattern = ctx.createPattern(img, 'repeat'); // 使用图案对象设置fillStyle或者strokeStyle属性 ctx.fillStyle = pattern ctx.rect(0, 0, canvas.width, canvas.height); ctx.fill();

填充渐变

使用渐变填充的第一步是创建渐变对象。绘图上下文为此提供了两个方法:createLinearGradient()createRadialGradient()

js
// 创建一个从(10, 0)到(100, 0)的渐变 var gradient = ctx.createLinearGradient(10, 0, 100, 0); // 添加两种颜色 gradient.addColorStop(0, 'magenta'); gradient.addColorStop(1, 'yellow'); // 调用一个自己写的函数来绘制一个心形 drawHerat(60, 50); // 填充心形 ctx.fillStyle = gradient; ctx.fill(); ctx.stroke();

这里是创建线性渐变,因此我们给createLinearGradient()传入两个坐标点,分别表示渐变的起点和终点。起点和终点构成了颜色逐渐过渡的区间。如果需要填充的对象超过这个区间,也就是超出了渐变的范围,那么其他地方的颜色将会变成实色。

一般来说,我们创建的渐变只要恰好比要填充的图形大一点即可。

确定了渐变线的宽度和角度之后(上传是水平的渐变,因为y坐标都 是0),接下来该实际地设置构成渐变的颜色了。要设置渐变颜色,需要使用渐变对象的addColorStop()方法。每次调用这个方法,都需要添加一个0-1的偏移值和一个颜色值。其中,偏移值决定颜色在渐变中的位置:0表示位于渐变的起点,1表示位于渐变的终点

js
var gradient = ctx.createLinearGradient(10, 0, 100, 0); gradient.addColorStop(0, 'magenta'); gradient.addColorStop(.25, 'blue'); gradient.addColorStop(.5, 'green'); gradient.addColorStop(.75, 'yellow'); gradient.addColorStop(1, 'red'); drawHeart(60, 200); ctx.fillStyle = gradient; ctx.fill(); ctx.stroke();

创建放射性渐变需要指定两个圆。因为放射性渐变就是颜色从一个小圆过渡到一个更大的、包含它的圆。要定义圆,需要提供圆心坐标和半径。

js
var gradient = ctx.createRadialGradient(180, 100, 10, 180, 100, 50); gradient.addColorStop(0, 'magenta'); gradient.addColorStop(1, 'yellow'); drawHeart(180, 80); ctx.fillStyle = gradient; ctx.fill(); ctx.stroke();

注意,把两个圆设置为同心圆是最常见的做法。不过,当然可以给内圆和外圆设置不同的圆心,这样可以实现拉伸、压缩或其他颜色变形的效果。

赋予图形交互能力

记录绘制的内容

为了修改和重绘画面,必须先知道要修改和重绘什么内容。

js
var circles = []; var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); function Circle (x, y, radius, color) { this.x = x; this.y = y; this.radius = radius; this.color = color; this.isSelected = false; } function randomFromTo (min, max) { return min + Math.round(Math.random() * (max - min)); } function addRandomCircle () { // 为圆圈计算一个随机大小和位置 var radius = randomFromTo(10, 60); var x = randomFromTo(0, canvas.width); var y = randomFromTo(0, canvas.height); // 为圆圈计算一个随机颜色 var colors = ['green', 'blue', 'red', 'yellow', 'megenta', 'orange', 'brown', 'purple', 'pink']; var color = colors[randomFromTo(0, colors.length - 1)]; // 创建一个新圆圈 var circle = new Circle(x, y, radius, color); // 把它保存在数组中 circles.push(circle); // 重新绘制画布 drawCircles(); } // 绘制圆圈 function drawCircles () { // 清除画面,准备绘制 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.globalAlpha = .85; ctx.strokeStyle = 'black'; // 遍历所有的圆 circles.forEach(function (circle) { // 绘制圆圈 ctx.beginPath(); ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI); ctx.fillStyle = circle.color; ctx.fill(); ctx.stroke(); }) } addRandomCircle(); drawCircles();

基于坐标的碰撞检测

只要创建交互图形,几乎就一定要用到碰撞检测,也就是测试某个点是否“碰到”了某个图形。在绘制圆圈的程序中,我们需要检测用户单击的点是否碰到某个圆圈,或者是否点击了空白区域。

为了做碰撞测试,就要检测每一个开关,计算鼠标点击的那个点是否落在某个形状里面。如果是,则说明单击“碰到”了该形状。

js
var circles = []; var previousSelectedCircle = null; var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); function Circle (x, y, radius, color) { this.x = x; this.y = y; this.radius = radius; this.color = color; this.isSelected = false; } function randomFromTo (min, max) { return min + Math.round(Math.random() * (max - min)); } function addRandomCircle () { // 为圆圈计算一个随机大小和位置 var radius = randomFromTo(10, 60); var x = randomFromTo(0, canvas.width); var y = randomFromTo(0, canvas.height); // 为圆圈计算一个随机颜色 var colors = ['green', 'blue', 'red', 'yellow', 'megenta', 'orange', 'brown', 'purple', 'pink']; var color = colors[randomFromTo(0, colors.length - 1)]; // 创建十个圆圈 var circle = new Circle(x, y, radius, color); // 把它保存在数组中 circles.push(circle); // 重新绘制画布 drawCircles(); } // 绘制圆圈 function drawCircles () { // 清除画面,准备绘制 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.globalAlpha = .85; ctx.strokeStyle = 'black'; // 遍历所有的圆 circles.forEach(function (circle) { // 绘制圆圈 ctx.beginPath(); ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI); ctx.fillStyle = circle.color; if (circle.isSelected) { ctx.lineWidth = 5; } else { ctx.lineWidth = 1; } ctx.fill(); ctx.stroke(); }) } addRandomCircle(); function handleClick (event) { var e = event || window.event; // 取得画面上被单击的点 var clickX = e.pageX - canvas.offsetLeft; var clickY = e.pageY - canvas.offsetTop; // 查找被单击的圆圈 for (var i = circles.length - 1; i >= 0; i--) { var circle = circles[i]; // 使用勾股定理计算这个点与圆心之间的距离 var distanceFromCenter = Math.sqrt(Math.pow(circle.x - clickX, 2) + Math.pow(circle.y - clickY, 2)); console.log(circle, clickX, clickY) if (distanceFromCenter <= circle.radius) { // 清除之前选择的圆圈 if (previousSelectedCircle != null) { previousSelectedCircle.isSelected = false; } previousSelectedCircle = circle; // 选择新圆圈 circle.isSelected = true; // 更新显示 drawCircles(); return; } } }

给Canvas添加动画

基本的动画

在HTML5中利用Canvas实现动画非常容易,只需要设置一个定时器(setTimeout()或setInterval()),反复调用(一般每次30-40次)绘图函数即可。每次调用,都会重会整个画面。

js
var canvas,ctx; window.onload = function () { canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); setTimeout(drawFrame, 20); function drawFrame () { ctx.clearRect(0, 0, canvas.width, canvas.height); // ... } }