canvas核心技术-如何绘制图片和文本

这篇是学习和回顾canvas系列笔记的第三篇,完整笔记详见:canvas 核心技术javascript

经过上一篇canvas核心技术-如何绘制图形的学习,咱们知道了如何绘制任意多边形以及图片的填充规则。在canvas中应用比较多的还有绘制图片和文本。这篇文章,咱们就来详细聊聊图片和文本的绘制。css

图片

在canvas中,咱们能够把一张图片直接绘制到canvas上,跟使用img标签相似,不一样的是,图片是绘制到canvas画布上的,而非独立的html元素。canvas提供了drawImage方法来绘制图片,这个方法能够有三种形式的用法,以下,html

  • void drawImage(image,dx,dy);直接将图片绘制到指定的canvas坐标上,图片由image传入,坐标由dx和dy传入。
  • void drawImage(image,dx,dy,dw,dh);同上面形式,只不过指定了图片绘制的宽度和高度,宽高由dw和dh传入。
  • void drawImage(image,sx,sy,sw,sh,dx,dy,dw,dh);这个是最复杂,最灵活的使用形式,第一参数是待绘制的图片元素,第二个到第五个参数,指定了原图片上的坐标和宽高,这部分区域将会被绘制到canvas中,而其余区域将忽略,最后四个参数跟形式二同样,指定了canvas目标中的坐标和宽高。

根据参数个数,咱们会分别调用不一样形式的drawImage,第一种形式最简单,就是将原图片直接绘制到目标canvas指定坐标处,图片宽高就是原图片宽高,不会缩放。第二种形式呢,指定了目标canvas绘制区域的宽高,那么图片最终被绘制在canvas上的宽高被固定了,图片会被缩放,若是指定的dw和dh与原图片的宽高不是等比咧的,图片会被压缩或者拉伸变形。第三种形式,分别指定了原图片被绘制的区域和目标canvas中的区域,经过sx,sy,sw,sh咱们可只选择原图片中某一部分区域,也能够指定完整的图片,经过dx,dy,dw,dh咱们待绘制的目标canvas区域。java

let img = document.createElement('img'); //建立img元素
img.src = './learn9/google.png'; //指定img的src
img.addEventListener(
  'load',
  () => {
    ctx.drawImage(img, 0, 0); // 将img元素调用drawImage(img,dx,dy)绘制出来
  },
  false,
);
复制代码

上面这个示例,这张Google图片的原始大小是544*184,而canvas区域的大小是默认的300*150。咱们调用了第一种形式,直接将图片绘制到canvas的坐标原点处,图片没有被缩放,超出了canvas区域,超出的部分,会被canvas忽略的。有一点须要注意的是,我是在图片的onload事件中才开始绘制的,由于图片没有加载完毕,直接绘制图片是无效的。下面的代码示例,我都将只贴出onload事件里的代码,图片加载部分代码都相同,就省略了。git

let canvasWidth = canvas.width; //获取canvas宽度
let canvasHeight = canvas.height; //获取canvas高度
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); 
复制代码

咱们把目标canvas区域指定为canvas的宽高,图片老是会被绘制在整个canvas中,同时也能够看到绘制出来的图片变形了。咱们能够经过计算出原图片的宽高比,根据canvas目标区域的宽度来计算出canvas目标区域的高度,或者根据canvas目标区域的高度来计算出canvas目标区域的宽度。github

let imgWidth = img.width; //获取图片的宽度
let imgHeight = img.height; //获取图片的高度
let targetWidth = canvasWidth; //指定目标canvas区域的宽度
let targetHeight = (imgHeight * targetWidth) / imgWidth; //计算出目标canvas区域的高度
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
复制代码

从图能够看到,根据图片宽高比计算出来的目标canvas区域,最终,图片绘制出来的效果是等比例缩放,没有变形。canvas

咱们再来看看最为复杂,且最为灵活的第三种方式。使用这种方式,咱们能够把Google这张图片中的红色的那个o部分绘制出来。数组

ctx.drawImage(img, 143, 48, 90, 90, 0, 0, 90, 90);
复制代码

Google这张图中,红色字母o在原图片中的坐标是(143,48),宽高是90*90,咱们简单的把这个字母绘制在了canvas的(0,0)坐标处,宽高也是90*90。能够再来复杂点,把这个红色的字母o,让它的高度跟canvas的高度同样,且等比例放大宽度,且圆心正好在canvas中心,实现以下,ide

let oWidth = 90; //获取字母o的宽度
let oHeight = 90; //获取字母o的高度
let targetHeight = canvas.height; //指定目标canvas区域的高度
let targetWidth = (oWidth * targetHeight) / oHeight; //计算出目标canvas区域的宽度
let targetX = (canvas.width - targetWidth) / 2; //移动目标canvas坐标X
ctx.drawImage(img, 143, 48, oWidth, oHeight, targetX, 0, targetWidth, targetHeight);
复制代码

drawImage返回的第一参数image,不只能够是图片元素,实际上还能够是canavs元素,video元素。常见的离屏canvas的使用,依就是将离屏不可见的canvas绘制到当前显示屏幕canvas上。离屏幕canvas这一部分将会在后续游戏部分中说到,这里不详细说了。函数

图像像素

跟图片绘制有关的函数还有3个,它们分别是getImageDataputImageDatacreateImageData。这些函数是直接能够改变图像中某一个具体的像素值,从而能够对图片作一些操做,好比滤镜。

咱们先来看看getImageData方法,它的调用方式是let imgData = ctx.getImageData(sx,sy,sw,sh),接受四个参数,表示canvas区域的某一个矩形区域,这个矩形区域的左上角坐标是(sx,sy),宽高是sw 和sh,它的返回值是一个ImageData类型的对象,包含的属性有widthheightdata

  • ImageData.width,无符号长整型,表示这个图像区域的像素的宽度。

  • ImageData.height,无符号长整型,表示这个图像区域的像素的高度。

  • ImageData.data,一个Uint8ClampedArray数组,数组里每4个单元,表示一个像素值。一个像数值用RGBA表示的,这4个单元分别表示R,G,B,A,表示意思是红,绿,蓝,透明度,取值范围是0~255。

须要注意的是,若是咱们在调用ctx.getImageData(sx,sy,sw,sh),参数表示的矩形区域超出了canvas的区域,那么超出的部分将是用黑色的透明度为0的RGBA值表示,也就是(0,0,0,0)。

let imgWidth = img.width; //获取图片的宽度
let imgHeight = img.height; //获取图片的高度
let targetWidth = canvasWidth; //指定目标canvas区域的宽度
let targetHeight = (imgHeight * targetWidth) / imgWidth; //计算出目标canvas区域的高度
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
console.log(`canvas.width = ${canvasWidth}`);
console.log(`canvas.height = ${canvasHeight}`);
console.log(imgData);
复制代码

能够看到,咱们的canvas默认宽高是300*150,经过ctx.getImageData获取整个canvas区域的像素数据值,获得的ImageData的设备像素的宽高也是300*150 ,Imagedata.data 的数组的长度是180000,这个是由于,这个imgData的像素数是300*150,而每一个像素是由4个份量表示的,因此300*150*4 = 180000了。

当咱们经过getImageData获得canvas某一个矩形区域的像素数据以后,咱们能够经过改变这个imageData.data数组里的颜色份量值,再将改变后的ImageData经过putImageData方法绘制到canvas上。putImageData的用法有2种调用形式,以下,

  • ctx.putImageData(imgData,dx,dy),这种方式,将imgData绘制到canvas区域(dx,dy)坐标处,绘制到canvas的区域的矩形大小就是imgData的矩形的大小。
  • ctx.putImageData(imgData,dx,dy,dirtyX,dirtyY,dirtyW,dirtyH),不只指定了canvas区域(dx,dy),也指定了imgData脏数据区域的(dirtyX,dirtyY)和宽高dirtyW,dirtyH。这种形式,能够只将imgData种某一块区域绘制到canvas上。
let canvasWidth = canvas.width;
let canvasHeight = canvas.height;
let img = document.createElement('img');
img.src = './learn9/google.png';
img.addEventListener(
 'load',
 () => {
   let imgWidth = img.width; //获取图片的宽度
   let imgHeight = img.height; //获取图片的高度
   let targetWidth = canvasWidth; //指定目标canvas区域的宽度
   let targetHeight = (imgHeight * targetWidth) / imgWidth; //计算出目标canvas区域的高度
   ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
   //操做ImageData像素数据
   let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
   oprImageData(imgData, (r, g, b, a) => {
     if (a === 0) {
       return [r, g, b, 255]; //将透明的黑色像素值改变为不透明
     }
     return [r, g, b, a];
   });
   //将imgData绘制到canvas的中心。超出canvas区域将被自动忽略
   ctx.putImageData(imgData, canvasWidth / 2, canvasHeight / 2);
 },
 false,
);

// 遍历像素数据
function oprImageData(imgData, oprFunction) {
 let data = imgData.data;
 for (let i = 0, l = data.length; i < l; i = i + 4) {
   let pixel = oprFunction(data[i], data[i + 1], data[i + 2], data[i + 3]);
   data[i] = pixel[0];
   data[i + 1] = pixel[1];
   data[i + 2] = pixel[2];
   data[i + 3] = pixel[3];
 }
}
复制代码

上面,咱们遍历了ImageData中data数组,并将透明度为0的像素值的透明度变为1(255/255=1)。在遍历像素数组时,咱们每便利一次,i 的值加4,这个是由于一个像素值是用4个数组单元值表示的,分别为R,G,B,A,咱们能够只改变某一个像素值的某一个份量值,例如透明度。

ctx.putImageData(imgData, canvasWidth / 2, canvasHeight / 2, 79, 27, 50, 50);
复制代码

咱们经过指定了ImageData中脏数据区域,只绘制了红色字母o,其余部分忽略。上面在调用putImageData以前,咱们经过遍历像素数据改变了部分像素值的透明度,这种能够操做像素值的方式,在图像处理等领域是很是有用的,例如常见的图像灰度和反相颜色等。

//操做ImageData像素数据
let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
ctx.clearRect(0, 0, canvasWidth, canvasHeight); //清除canvas
oprImageData(imgData, (r, g, b, a) => {
    return [255 - r, 255 - g, 255 - b, a]; //反相颜色
});
ctx.putImageData(imgData, 0, 0);
复制代码

将颜色份量的RGB值都用255减去原颜色份量值,能够看到,Google每一个字母的颜色都与原图片的颜色不同了。这个在改变每一个颜色份量的值,用不通的逻辑计算,就能够获得不一样的处理后的图片。

//操做ImageData像素数据
let imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
ctx.clearRect(0, 0, canvasWidth, canvasHeight); //清除canvas
oprImageData(imgData, (r, g, b, a) => {
    let avg = (r + g + b) / 3;
    return [avg, avg, avg, a]; //灰度
});
ctx.putImageData(imgData, 0, 0);
复制代码

经过取RGB的平均值,原图片的每一个字母都是灰色的了,固然,在计算的时候,能够给每一个份量加一个系数,例如公式let avg = 0.299r + 0.587g + 0.114b,具体应用能够查看Grayscale

最后来看看createImageData,这个很好理解了,就是建立一个ImageData 对象了,有两种形式,以下,

  • ctx.createImageData(width,height),能够指定宽高,建立一个ImageData对象,ImageData.data中的像素值都是一个透明的黑色,也就是(0,0,0,0)。
  • ctx.createImageData(imgData),能够指定一个已经存在的ImageData 对象来建立一个新的ImageData对象,新建立的ImageData对象的宽高与参数中的ImageData 的宽高同样,可是像素值就不同了,新建立出来的ImageData的像素值都是透明的黑色,也就是(0,0,0,0)。

文本

在canvas中,咱们不只能够绘制图形,图片,还能够绘制文本。绘制文本比较简单了,先设置当前ctx的画笔的文本样式,例如,字体大小,字体样式,对其方式等,跟css中比较类似。

跟文本相关的方法有三个,以下,

  • strokeText(text,x,y,maxWidth?),用描边的形式绘制指定的文本text,其中也指定了绘制的坐标(x,y), 还有最后一个可选参数,最大的宽度,若是所绘制的文本超过了指定的maxWidth,则文本会按照最大的宽度来绘制,那么文字之间的间距就将减小,文字可能被压缩。
  • fillText(text,x,y,maxWidth?),同strokeText同样,只不过,是用填充的形式绘制文本,其参数含义同样。
  • measureText(text),在当前的文字样式下,测量绘制文本text会占据的宽度值,返回一个对象,这个对象有一个width属性。主要注意的是,必须先设置文本样式,再来测量才是准确的。

跟文本直接相关的属性设置,以下,

  • font,同css中含义同样,能够指定文本的字体大小,字体集,字体样式等。但在canvas中,line-height被强制设置为normal,会忽略其余设置的值。
  • textAlign,设置文本的水平对其方式,可选值有:leftrightcenterstartend。默认值是start。各个含义参见textAlign取值
  • textBaseline,设置文本的垂直对齐方式,可选值有:tophangingmiddlealphabeticideographicbottom。默认值是alphabetic。各个含义参见textBaseline取值

固然了,还有一些其余的属性也会影响到文本最终绘制出来的效果,好比给当前ctx添加阴影效果,或者设置fillStyle的样式能够是图片或者渐变等。这些算是全局的属性设置,会影响到canvas全部其余的绘制,而不单单是文本,因此在这里,就不详细讨论了。

let textAligns = ['left', 'right', 'center', 'start', 'end']; //textAlign的取值
let colors = ['red', 'blue', 'green', 'orange', 'blueviolet']; //描边颜色
ctx.font = '18px sans-serif'; //设置font
for (let [index, textAlign] of textAligns.entries()) {
  ctx.save();
  ctx.textAlign = textAlign; // 设置textAlign
  ctx.strokeStyle = colors[index]; //设置描边颜色
  ctx.strokeText(textAlign, width / 2, 20 + index * 30); //使用描边绘制文本
  ctx.restore();
}
复制代码

咱们把textAlign的各个属性全都设置了一遍,看到startleft的效果同样,endright的效果同样,这个是由于startend是与当前本地文字开始方向有关的,若是是左到右开始,那么startleft同样,而若是是右到左开始,那么start是与right效果同样了。

let textBaselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];
let colors = ['red', 'blue', 'green', 'orange', 'blueviolet', 'cyan']; //描边颜色
ctx.font = '18px sans-serif'; //设置font
for (let [index, textBaseline] of textBaselines.entries()) {
  ctx.save();
  ctx.textBaseline = textBaseline; // 设置textBaseline
  ctx.strokeStyle = colors[index]; //设置描边颜色
  ctx.strokeText('abj', 10 + index * 50, height / 2); //使用描边绘制文本
  ctx.restore();
}
复制代码

咱们又把textBaseline的各个值全设置了一遍,看到的效果如上图。用到最多的应该是topmiddlealphabeticbottom了,其中默认值是alphabetic

measureText在实际业务中也是用到比较多的一个方法了,这个方法能够测量出在当前设置的文本样式下,绘制指定的text会占据的宽度。特别是在绘制表格数据,或者一些分析图时,须要绘制说明提示性文本,可是又想根据当前鼠标位置来决定文本绘制的坐标,以避免超出canvas可见区域。这个方法使用比较简单,会返回一个带有width属性的对象,这个width属性值就是测量出来的结果。在canvas没有测量文本高度的方法,然而,在实际时,经常会以W字母测量出来的宽度值加上一点点,就能够大体认为是当前文本的高度值了。

ctx.font = '18px sans-serif'; //设置font,必定得先设置font属性,才能测量准确
let textWidth = ctx.measureText('W').width;
let textHeight = textWidth + textWidth / 6;
console.log(`当前文本W的宽度:${textWidth}`);
console.log(`当前文本W的高度:${textHeight}`);
复制代码

小结

这篇文章主要是学习了canvas中如何使用drawImage来绘制图片,以及如何使用getImageDataputImageData来对图像像素值作处理,好比常见的图片灰度处理,或者反相颜色等。也回顾了在canvas中绘制文本的一些相关方法和属性,这些知识在css中比较相似,理解起来也比较容易和简单。

相关文章
相关标签/搜索