1.描述
首先混个脸熟,canvas?干啥的,为啥要用。打开疑问:canvas画图的。问:有图片,有我们漂亮的UI设计师,为什么要自己画?答:canvas可以动态生成图片,虽然你可以拿到各式各样的图片,但是你能拿到我想要的文字加你的图片吗?显然不能。比如面对不同的用户,都显示xxx,早上好。普通的照片是做不到,需要我们自己手动去画,常见的是文字配二维码。后面有实例不过多详细说明。本篇文章主要基于对canvas的初体验,对于小白前端而言“或许”就够了,如果需要深入学习请前往官或者网书籍等详细学习,谢谢。
2.常见的2d画图工具canvas与svg比较
| 比较 | canvas | svg |
|---|---|---|
| 维度 | 2d | 2d |
| 类型 | 位图 | 矢量图 |
| 图形 | 纯代码(js) | 标签 |
| 操作 | 麻烦 | 简便 |
| 性能 | 性能极高 | 一般:跟html标签 |
| 用途 | 高性能 | 重交互 |
| 应用场景 | 游戏、大型图表、图片处理 | 地图、小型图表、图标 |
解释:
canvas:位图(存的是像素——放大模糊),所有代码路径操作全部是由js完成,在画30000个路径以上时就会有一点点卡顿。
svg:矢量图(存的是路径——放大依然清晰),所有代码由html标签完成,在html中会渲染很长时间,一般画2000个标签时就会卡顿。
总结:各有优缺点,canvas可以说是svg的十多倍性能,以下以canvas为主,想学习svg请自行学习。
3.绘图样式、操作顺序
- canvas标签
#c1 { border: 1px dashed black; display: block; margin: 5px auto 0; }
<canvas id="c1" width="800" height="600"> 你的浏览器不支持canvas,请升级或更换 <a href="chrome.google.com/">下载 </canvas>
内容:只有浏览器不识别canvas才会显示canvas标签中的内容。width、height必须是属性 决定了canvas的绘图范围viewbox,style加给canvas——拉伸canvas,这句话什么意思呢?在canvas中,只能在标签内定义宽高,不能再style中定义宽高,就算定义了,也只是放大或者缩小,没有意义。
- 图形上下文
const gd=canvas.getContext('type') type: 2d, webgl
- 图形操作-canvas中一切是路径
路径——选定范围(不会真的画东西),后续绘图操作(stroke、fill)
绘制——直接画出东西 stroke
- 线(一切都是点位操作)
moveTo(x,y) 起点
lineTo(x,y) 终点(多次划线也是下一笔的起点)
closePath() 手动闭合路径
- 绘图操作
stroke() 边框
fill() 填充:自动闭合(最后一个路径点,到起点)
4.Line操作、路径操作
- 路径
moveTo
lineTo
closePath
2.绘图
stroke
Fill
边线颜色 strokeStyle
边线宽度 lineWidth
填充颜色 fillStyle
strokeStyle=任何css颜色
名称:red、blue、green...
16进制:#C00、#CC0000
rgba:rgb(x,x,x)、rgba(x,x,x,1)
eg: gd.strokeStyle = "rgba(255, 0, 255, 0.1)";
【!!!顺序的重要性】
strokeStyle需要在stroke()的前面
strokeStyle只会影响后续的操作,对于已经完成的操作完全没影响
样式:
1.strokeStyle // 描边样式
2.fillStyle // 填充样式
3.lineWidth // 线宽
!!!重要:顺序
上代码:
let canvas = document.getElementById('c1')
let gd = canvas.getContext('2d')
gd.moveTo(100, 100);
gd.lineTo(300, 200);
gd.lineTo(100, 200)
gd.strokeStyle = "rgb(255, 0, 255)"; // 线条颜色
gd.fillStyle = 'red'; // 填充颜色
gd.lineWidth = 5; // 线宽
gd.closePath() // 闭合
gd.fill() // 填充
gd.stroke();
5.渐变
- 创建线性渐变对象
gd.createLinearGradient(x1, y1, x2, y2) // 点位代表起点、终点的渐变过程
let canvas = document.getElementById('c1')
let gd = canvas.getContext('2d')
gd.moveTo(100, 100);
gd.lineTo(300, 200);
gd.lineTo(100, 300);
// 创建线渐变对象
const gradient = gd.createLinearGradient(
100, 100,
300, 200
);
// 添加渐变点
gradient.addColorStop(0, '#fff');
gradient.addColorStop(0.24, '#f00');
gradient.addColorStop(1, '#0f0');
gd.fillStyle = gradient;
gd.fill();
gd.stroke();
-
创建圆形渐变 gd.createRadialGradient(x1, y1, r1,x2, y2, r2)
let canvas = document.getElementById('c1') let gd = canvas.getContext('2d') gd.rect(50,50,600,400) // 画了一个矩形描边 const gradient = gd.createRadialGradient( 100, 100, 100, 300, 200, 50 ); //添加渐变点 gradient.addColorStop(0, '#CCC'); gradient.addColorStop(0.24, '#f00'); gradient.addColorStop(1, '#0f0'); gd.fillStyle = gradient; gd.fill(); gd.stroke();
嗯?很懵,咋做到的。来看一下原理
原理: 两个点位圆相切,两个圆心之间的连线为0.5 一看就懂了,不多说。
6. 文字
1.绘制文字
gd.fillText(txt, x, y) // 真实的写文字
gd.strokeText(txt, x, y) // 这个只是描边文字
2.font属性
gd.font='20px 字体';
gd.font='bold italic 20px 字体';
和css一样,一看就懂
3.默认原点(左上角)
但是中文文字在左下角
gd.font='bold italic 20px 字体';
gd.fillText('你好',0,20) // 真实的写文字 20为字体大小
gd.strokeText('你好', 0, 40) // 这个只是描边文字
4.文字原点
默认:left alphabetic
gd.textAlign = 'left'||'center'||'right'; // 文字对齐方式
注意这里的对齐方式,不是文字居中画布,而是中心点位于文字的左中右。
gd.textBaseline = 'alphabetic'||'bottom'||'top'||'middle'; 文字基线
alphabetic:在英文的时候y、g等会超出下面一部分,这个基线就是针对英文的正常基线。
- 多行文字 描述: 当有多行文本的时候怎么办呢?canvas不会自动换行,不可能一个像素一个像素取算吧?
measureText(str)为canvas的一个方法可以计算文字的长度,想一想,如果有文字长度加上画布长度,是不是就可以每一行放多少了呢?
上代码:
const fontSize = 20;
const lineHeight = fontSize \* 2;
gd.font = `${fontSize}px 宋体`;
gd.textAlign = 'center';
gd.textBaseline = 'top';
const str = '声明显示,深圳市智信新信息技术有限公司,由深圳市智慧城市科技发展集团与30余家荣耀代理商、经销商共同投资设立,包括天音通信有限公司、苏宁易购集团股份有限公司、北京松联科技有限公司、深圳市顺电实业有限公司、山东怡华通信科技有限公司、深圳冀顺通投资有限公司、河南象之音健康科技有限公司、福建瑞联优信科技有限公司、内蒙古英孚特通讯技术有限公司、科技发展有限公司等。';
const lines = [];
let line = '';
for (let i = 0; i < str.length; i++) {
if (gd.measureText(line + str[i]).width > canvas.width) {
lines.push(line);
line = '';
}
line += str[i];
}
lines.push(line);
lines.forEach((line, index) => {
gd.fillText(line, canvas.width / 2, lineHeight * index + (lineHeight - fontSize) / 2);
});
- 线
lineCap (划线的时候头部样式)
'butt' 默认 没有
'round' 圆
'square' 正方形
lineJoin (链接样式)
'miter' 默认
'bevel' 折叠
'round'
eg: 以bevel折叠为例
gd.beginPath();
gd.moveTo(100, 100);
gd.lineTo(300, 200);
gd.lineTo(100, 300);
gd.lineWidth=10
gd.lineCap='square'
gd.lineJoin = 'bevel'
gd.stroke()
gd.beginPath()
gd.moveTo(300, 300);
gd.lineTo(400, 200);
gd.lineTo(600, 300);
gd.lineWidth=10
gd.lineCap='square'
gd.lineJoin = 'round'
gd.stroke()
理解路径
路径——操作范围
gd.moveTo(100, 100);
gd.lineTo(300, 200);
gd.lineWidth=10
gd.strokeStyle='red'
gd.stroke()
// gd.beginPath()
gd.strokeStyle='yellow'
gd.moveTo(300, 300);
gd.lineTo(400, 400);
gd.lineWidth=10
gd.stroke()
放开注释
为什么需要这一步? 你那一支笔,不换一个颜色就是上面的,如果画好了,在拿黄笔画就是第二张图
beginPath——清除已有的路径(开始一个全新的)
习惯:一定先beginPath,然后再开始操作路径
canvas绝大多数的属性,都是“全局” 比如: gd.globalAlpha=0.2 设置全局透明度,干啥?用来设置后续操作的透明度(绘制完要恢复回来)
7.阴影
1.颜色 shadowColor
2.范围 shadowBlur
3.偏移 shadowOffsetX,shadowOffsetY
let canvas = document.getElementById('c1')
let gd = canvas.getContext('2d')
gd.shadowColor = 'rgba(0,0,0,1)';
gd.shadowBlur = 4;
gd.shadowOffsetX = 4;
gd.shadowOffsetY = 4;
drawRect(100, 100, 400, 300,'red');
drawRect(20, 200, 30, 500,'yellow');
drawRect(150, 200, 10, 300,'green');
// 自己写的方法,可以用gd.fillRect()直接画矩形
function drawRect(x, y, w, h,color) {
gd.beginPath();
gd.fillStyle=color
gd.moveTo(x, y);
gd.lineTo(x + w, y);
gd.lineTo(x + w, y + h);
gd.lineTo(x, y + h);
gd.closePath();
gd.fill();
}
8.画椭圆、圆、饼图
- 圆/弧 注意(平常我们画圆起始点在顶上为(0,0),在canvas中正右方为0,0) 如果需要调整为肉眼的正上方需要回转90°,后面例子可见
ellipse 椭圆弧 (如果会画椭圆,name就会画圆,x=y=r)
gd.ellipse(
//圆心 cx, cy,
//半径 rx, ry,
//顺时针旋转 rotation,
//角度 startAng, endAng,
//是否逆时针 anticlock(可选参数) )
arc 正圆弧
圆没有旋转概念,怎么旋转都一样
gd.arc( cx, cy, r, startAng, endAng, [anticlock](可选参数) )
gd.beginPath()
gd.ellipse(
300,300,200,100,degree2arc(20),0,degree2arc(360) //??? 什么鬼
)
gd.stroke()
gd.beginPath()
gd.arc(
500,100,100,0,degree2arc(360) // ?? 什么鬼
)
gd.stroke()
// 度->弧度
function degree2arc(n) {
return n * Math.PI / 180;
}
// 弧度->度
function arc2degree(n) {
return n * 180 / Math.PI;
}
在上面看到代码有??了吗?现在回忆一下学数学那会儿,圆都是弧度,所以计算机需要转一下
度 0~360
弧度 0~2*PI
换算:
360度=2*PI弧度
1度=2*PI/360弧度=PI/180弧度
n度 = n*PI/180弧度
2*PI弧度=360度
1弧度=180/PI度
n弧度=n*180/PI度
度->弧度 n*PI/180
代码写了两个函数就是这个意思,如果不套用函数gd.ellipse( 300,300,200,100,degree2arc(20),0,360) )其实有很多个圆,请自行尝试
- 饼图
思考,怎么画饼图呢?既然能画弧,那必然有参数可以画半圆弧。没问题,尝试一下
gd.beginPath()
gd.moveTo(200,300)
gd.arc(
300,300,100,degree2arc(0-90),degree2arc(90-90)
)
gd.stroke()
// 第二个
gd.beginPath()
gd.moveTo(400,500)
gd.arc(
500,500,100,degree2arc(0-90),degree2arc(90-90) // 上面注意说了,canvas原点在最右边,-90就是直观的顶点为起点
)
gd.closePath() 闭合
gd.stroke()
既然扇形出来了,那饼图不就简单啦。举个例子
饼图
100, 50, 200 350
//占比
28.57%
14.28%
57.14%
//角度
102.85°
51.4°
205.7°
//start, end
0, 102.85
102.85, 154.25
154.25, 360
上面大家都懂,不说了 上代码
const cx = canvas.width / 2,
cy = canvas.height / 2,
r = 200;
//数据
const data = [
{ value: 50, color: 'red' },
{ value: 150, color: 'green' },
{ value: 200, color: 'pink' },
{ value: 300, color: '#CCC' },
{ value: 500, color: 'yellow' },
];
let total = 0;
data.forEach(item => {
total += item.value;
});
let base = 0;
data.forEach(item => {
let ang = 360 \* item.value / total;
drawPie(base, base + ang, item.color);
base += ang;
});
function drawPie(startAng, endAng, color) {
gd.beginPath();
gd.moveTo(cx, cy);
gd.arc(
cx, cy, r,
degree2arc(startAng - 90), degree2arc(endAng - 90),
false,
);
gd.closePath();
gd.fillStyle = color;
gd.fill();
}
如果再想配文字,就自己去配了,当然这只是原理,如果使用饼图,可以看一下echarts,这里就不描述了。
9.曲线
1.贝塞尔曲线
bezierCurveTo( x1, y1, x2, y2, x, y ) 相信大家都了解ps钢笔绘图吧,那个就是贝塞尔曲线,起点到终点画图,两个控制点。
gd.beginPath();
gd.moveTo(100, 100); 起点
gd.bezierCurveTo(
300, 100, // 控制点1
100, 300, // /控制点2
300, 300 // 终点
);
gd.stroke();
原理如下(感兴趣可以去研究一下)这里就不说了
2.曲线
quadraticCurveTo( x1, y1, x, y )一个结束点,一个控制点
gd.beginPath();
gd.moveTo(100, 100); // 起点
gd.quadraticCurveTo(
280, 100, // 控制点,可以想象这里是一个磁铁,在吸引
300, 300 // 结束点
);
gd.stroke();
总结:这里不是几何学,如果喜欢曲线可以自行去学习,这里了解即可。
10.开发中canvas实战,加上画图片
在h5中二维码保存:
// 这个是文字加二维码
// 这张图是由图片加画二维码加文字
在手机上请把二维码保存到相册,带上这条数据的动态标题(最开始就说了,图片做不到吧。需要手动画)
直接上代码:(这里以第二张图为例)
// 这里有两种方法,第一是直接用canvas画图片,第二是创建canvas然后转换为地址,在放到img中去。这里我使用第二种,为什么呢?因为在手机上,canvas没法保存,只能长按图片保存,所以采用第二种,转换canvas为图片src塞到img中
<van-overlay :show="showQrCode" z-index="100" @click="showQrCode = false">
<div id="container" class="image-code">
<div>
<img id="qrcode" class="qr" :src="temUrl" alt >
<div class=" flex justify-center margin-top">
<span style="color: white;">
请长按保存图片
</span>
</div>
</div>
</div>
</van-overlay>
async checkQr () {
this.showQrCode = true
let url = 'xxx,写自己的地址及参数'
const res = await createQrCode({ // 这里和后端配合拿到二维码,带logo的,如果不带logo可以使用三方组件qrCode包,生成一个二维码地址
contents: url,
width: 240,
height: 240
})
this.createCanvas(res)
},
async createCanvas () {
let canvas = document.createElement('canvas')
// 获取到屏幕倒是是几倍屏。 这里做适配
let getPixelRatio = function (context) {
let backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1
return (window.devicePixelRatio || 1) / backingStore
}
// iphone6下得到是2
const pixelRatio = getPixelRatio(canvas)
// // 设置canvas的真实宽高
let canvasCtx = canvas.getContext('2d')
let img = document.createElement('img')
// 这里看个人,根据ui去量
canvas.width = pixelRatio * 120
canvas.height = pixelRatio * 147
// 获取二维码code,和后端配合
const res = await getMiniCode({
scene: this.$route?.query?.orgId,
page: 'pagesSecondHouse/shop/shop/index',
appId: 'wx0e3787020d00b980'
})
img.src = res
let image = new Image()
// 插入底图
image.src = 'xxx.png'
// 再画网络url图片时,请加上下面这句话,不然canvas告诉你跨域了,下面代码意思是允许跨域
image.setAttribute('crossOrigin', 'Anonymous')
setTimeout(async () => {
canvasCtx.beginPath()
//画底图
canvasCtx.drawImage(image, pixelRatio * 6, pixelRatio * 0, pixelRatio * 110, pixelRatio * 137)
canvasCtx.beginPath()
canvasCtx.font = '15px 宋体'
canvasCtx.fillStyle = '#fff'
canvasCtx.textAlign = 'center'
canvasCtx.fillText(this.$route.query?.orgName, (canvas.width / 2), pixelRatio * 33)
canvasCtx.beginPath()
// 画二维码
canvasCtx.drawImage(img, pixelRatio * 30, pixelRatio * 40, pixelRatio * 60, pixelRatio * 60)
// 将画好的canvas装换成src用img展示temUrl
this.temUrl = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream')
}, 1000)
},
// 到此就已经画出来了
!!! 如果实在网页端,可以点击保存canvas图片就不用转img了,直接上canvas
// 网页端保存图片调用方法
// url 请使用let url=canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream') 生成
this.saveImgFile( url, filename || `file_${new Date().getTime()}.png` )
saveImgFile (data, filename) {
let eleLink = document.createElement('a') // 下面就是保存的步骤了,很简单 自行看看
eleLink.href = data // 转换后的图片地址
eleLink.download = 'xx.jpg' // 自己取个名字
document.body.appendChild(eleLink)
// 触发点击
eleLink.click()
// 然后移除
document.body.removeChild(eleLink)
全文总结:如果不涉及复杂业务,你掌握这篇canvas对你“或许”够了,如果还需要深华,请自行学习。