文章目录
Canvas 基础教程
参考:canvas基础
canvas绘制矩形
canvas
标签只支持一种原生的图像绘制,那就是矩形。其他图像的绘制至少都需要生成一条路径。
//绘制图像的样式
ctx.globalAlpha = .2// 画布全局透明度设置为 0.2
ctx.fillStyle= 'red' //设置图像的填充颜色。(默认黑色)
ctx.strokeStyle= 'blue' //设置图像轮廓的颜色。(默认黑色)
ctx.lineWidth=2 //设置当前绘线的粗细,属性值必须为正数,2px
ctx.lineJoin = 'round' //设定线条与线条间结合的样式。(默认miter)round:圆角。bevel:斜角。miter:直角。
ctx.fillRect(x, y, width, height) *// 填充矩形*
ctx.strokeRect(x, y, width, height) *// 边框矩形*
ctx.clearRect(x, y, width, height) //可以清除 canvas 画布上指定的区域
strokeRect
渲染时的默认边框应该是 1px, 但是 canvas
在渲染矩形边框时,边框宽度是平均分在偏移位置两侧的。导致:
// 边框会渲染在 49.5-50.5 之间,浏览器是不会让一个像素只显示一半的,只会全部显示。相当于边框会渲染在 49-51 之间,也就是 2px
ctx.strokeRect(50, 50, 100, 100)
// 将偏移量多移动 0.5,边框会渲染在 200-201 和 50-51 之间,也就是1px
ctx.strokeRect(200.5, 50.5, 100, 100)
canvas路径
// 绘制路径不显示,相当于只是形成了路径列表,要调用 fill() 或 stroke() 方法才会呈现在画布中。
ctx.rect(50, 50, 100, 100)
ctx.fill()// 填充显示,会自动封闭当前路径,然后填充
ctx.rect(200, 50, 100, 100)
ctx.stroke()// 路径连接显示
ctx.beginPath() //新建一条路径,通常我们在绘制图像之前,都会调用该方法。
ctx.lineCap = 'round' //绘制每一条线段末端的样式属性。butt:线段末端以方形结束。(默认值);round:线段末端以圆形结束;square:线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段宽度一半的矩形区域。
ctx.moveTo(70,70)
ctx.lineTo(90,100)
ctx.lineTo(50,160)
ctx.closePath() //手动闭合路径
ctx.stroke() //调用stroke才能把上述路径绘制出来
canvas圆弧
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)
:画一个以(x, y)坐标为圆心,radius 为半径的圆弧或圆,从 startAngle 开始,到 endAngle 结束。
- startAngle:圆弧的起始点,x 轴方向开始计算,单位以弧度表示。
- endAngle:圆弧的终点,单位以弧度表示。
- anticlockwise:true 表示逆时针,false 表示顺时针。(默认值)
贝塞尔曲线
二次贝塞尔曲线
ctx.quadraticCurveTo(cpx, cpy, x, y)
- cpx:控制点的 x 轴坐标。
- cpy:控制点的 y 轴坐标。
- x:终点的 x 轴坐标。
- y:终点的 y 轴坐标。
三次贝塞尔曲线
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
参数只是在二次贝塞尔的基础上多增加了一个控制点 (cp2x, cp2y)。
// 将4个点连接起来
ctx.beginPath()
ctx.moveTo(20, 20)
ctx.lineTo(200, 0)
ctx.lineTo(100, 100)
ctx.lineTo(200, 100)
ctx.stroke()
// 二次贝塞尔曲线在3点之间的位置
ctx.beginPath()
ctx.strokeStyle='blue'
ctx.moveTo(20, 20)
ctx.quadraticCurveTo(200, 0, 100, 100, 20)
ctx.stroke()
// 三次贝塞尔曲线在4点之间的位置
ctx.beginPath()
ctx.strokeStyle='purple'
ctx.moveTo(20, 20)
ctx.bezierCurveTo(200, 0, 100, 100, 200, 100, 20)
ctx.stroke()
在这里插入图片描述
绘制文字
- ctx.fillText(text, x, y, [maxWidth]):在 (x, y) 填充指定的文本 (text)。
- ctx.strokeText(text, x, y, [maxWidth]):在 (x, y) 绘制文本边框 (text)。
// 边框文本
ctx.font = '30px sans-serif'
ctx.strokeText('天天好心情', 50, 50)
// 填充文本
ctx.textAlign='center'//文本对齐方式 'left' 'right' 'center' 'start' 'end'
ctx.fillText('天天好心情', 50, 100)
// 填充文本现在宽度
ctx.textBaseline = 'middle'//当前文本基线的属性,alphabetic:文本基线是标准的字母基线。(默认值);top:文本基线在文本块的顶部。middle: 文本基线在文本块的中间。bottom:文本基线在文本块的底部。hanging:文本基线是悬挂基线。ideographic:文本基线是表意字基线。
ctx.fillText('天天好心情', 50, 150, 30)//绘制字体宽度超出最大宽度会水平自适应。
ctx.measureText('天天好心情')//返回一个 TextMetrics 对象,包含关于文本尺寸的信息(一般都用来获取文本的宽度)
ctx.textAlign = ‘center’ 比较特殊, 它是以x作为基准,所以,如果你想让文本在整个 canvas 居中,就需要将 (fillText / strokeText) 的x值设置成 canvas 的宽度的一半。
canvas变换
canvas变换是对坐标系的一种变换
ctx.translate(x, y):对当前 canvas
画布进行平移变换。在 canvas
中 translate 是累加的。
ctx.rotate(angle):将当前 canvas
画布对照原点顺时针旋转。angle是弧度
ctx.scale(x, y):将当前 canvas
画布的 x 轴和 y 轴进行伸缩变换,负数则变化方向
背景图片
ctx.drawImage(img, sx, sy, swidth, sheight, x, y, width, height)
:在画布上绘制图片。
参数:
img:图像源对象(规定使用的图像、画布或视频等)。
sx:可选,开始剪切的x坐标位置。
sy:可选,开始剪切的y坐标位置。
swidth:可选,被剪切图像的宽度。
sheight:可选,被剪切图像的高度。
x:在画布上放置图像的x坐标位置。
y:在画布上放置图像的y坐标位置。
width:可选,要使用的图像的宽度(伸展或缩小图像)。
height:可选,要使用的图像的高度(伸展或缩小图像)。
const img = new Image()
img.src = './img/react.png'
//必须要等图片加载完才能操作
img.onload = () => {
// 将图片绘制到画布上
ctx.drawImage(img, 0, 0)
}
设置背景
ctx.createPattern(image, repetition)
:创建一个用于图像绘制使用的样式。
参数:
- image:图像源对象(规定使用的图像、画布或视频等)。
- repetition:重复图像的方式,值只能是 repeat | repeat-x | repeat-y | no-repeat。
- repetition 如果为空字符串 (’’) 或 null (但不是 undefined ),repetition将被当作 repeat。
// 创建背景样式
const pat = ctx.createPattern(img, 'repeat')
ctx.fillStyle = pat
ctx.fillRect(0, 0, 300, 100)
双缓存
画布已经清空了,但新的图片还没绘制完成,就造成了视觉上的闪烁或者空白
function drawImage(url, mainCanvas) {
//1. 第一步加载图片
const img = new Image();
img.src = url;
img.onload = () => {
//2.第二步,将图片绘制到缓存画布上,缓存画布临时创建和存储起来都可以
const cacheCanvas = document.createElement("canvas");
//画布设置为等大
cacheCanvas.width = mainCanvas.width;
cacheCanvas.height = mainCanvas.height;
//这里是绘制图片的逻辑,自适应或者拉伸取决于自己需要,为了方便这里就拉伸了
cacheCanvas.getContext("2d").drawImage(img, 0, 0, cacheCanvas.width, cacheCanvas.height);
//3.第三步,把缓存画布的内容绘制到主画布上
const mainCtx = mainCanvas.getContext("2d");
//先清空上一帧
mainCtx.clearRect(0, 0, mainCanvas.width, mainCanvas.height);
//绘制画面
mainCtx.drawImage(cacheCanvas, 0, 0);
}
}
canvas渐变
线性渐变
ctx.createLinearGradient(x1, y1, x2, y2)
:从 (x1, y1) 到 (x2, y2) 进行渐变。该方法返回一个 CanvasGradient
对象。使用 CanvasGradient 身上的 addColorStop(position, color)
设置渐变颜色。
参数:
- position:介于 0-1 之间的值,表示渐变中开始与结束之间的位置。
- color:在position位置显示的css颜色值。
// 从 (0, 0) 坐标点到 (300, 0) 坐标点进行渐变
const line = ctx.createLinearGradient(0, 0, 300, 0)
// 渐变顺序 红 --> 蓝 --> 绿
line.addColorStop(0, 'red')
line.addColorStop(.5, 'blue')
line.addColorStop(1, 'green')
// 图像填充颜色设置为渐变色
ctx.fillStyle = line
ctx.fillRect(0, 0, 300, 100)
径向渐变
ctx.createRadialGradient(x1, y1, r1, x2, y2, r2)
:从 (x1, y1) 为圆心,半径为 r1 的圆,向 (x2, y2) 为圆心,半径为 r2 的圆进行径向渐变。使用方法跟上述的 createLinearGradient 一样。
// 以 (200, 100) 为圆心 50 为半径,向 100 为半径的圆渐变
const grad = ctx.createRadialGradient(200, 100, 50, 200, 100, 100)
// 渐变顺序 红 --> 蓝 --> 绿
grad.addColorStop(0, 'red')
grad.addColorStop(.5, 'blue')
grad.addColorStop(1, 'green')
// 图像填充颜色设置为渐变色
ctx.fillStyle = grad
ctx.fillRect(0, 0, 400, 200)
canvas阴影
设置 canvas 图像或文字阴影需要如下属性:
ctx.shadowOffsetX
:图像 x 轴延伸距离。(默认值 0)
ctx.shadowOffsetY
:图像 y 轴延伸距离。(默认值 0)
ctx.shadowBlur
:用来设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响。(默认值 0)
ctx.shadowColor
:必须是标准的CSS颜色值,用于设定阴影颜色效果。(默认是全透明的黑色)
canvas状态
ctx.save()
:将当前状态放入栈中,保持 canvas 全部状态的方法。
保存到栈中的绘制状态由下面部分组成。①当前的变换矩阵②当前的剪切区域③当前的虚线列表④绘制图像的样式(strokeStyle / fillStyle / lineWidth / lineJoin / lineCap …)。
ctx.restore()
:通过在绘图状态栈中弹出顶端的状态,将 canvas 恢复到最近的保存状态的方法,如果没有保存状态,此方法不做任何改变。
通常我们在绘制图像进行的操作,都会放在 save() 和 restore() 方法之间,避免当前绘制图像设置的状态,影响到后续图像的绘制效果。
canvas图像合成设置
ctx.globalCompositeOperation
:设置或返回如何将一个源(新的 source)图像绘制到目标(已有的 destination)的图像上。可选值如下:
属性值 | 描述 |
---|---|
source-over | 源在上面,新的图像层级比较高。(默认值) |
source-in | 只留下源与目标的重叠部分。(源的那一部分) |
source-out | 只留下源超过目标的部分。 |
source-atop | 砍掉源溢出的部分。 |
destination-over | 目标在上面,旧的图像层级比较高。 |
destination-in | 只留下源与目标的重叠部分。(目标的那一部分) |
destination-out | 只留下目标超过源的部分。 |
destination-atop | 砍掉目标溢出的部分。 |
lighter | 显示源图像 + 目标图像。(重叠图形的颜色是通过颜色值相加来确定的) |
copy | 显示源图像,忽略目标图像。 |
xor | 那些重叠和正常绘制之外的其他地方是透明的 |
let canvas = document.querySelector('#canvas');
let ctx = canvas.getContext('2d');
let div = document.querySelector('.text');
canvas.width = div.clientWidth;
canvas.height = div.clientHeight;
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
canvas.addEventListener('mousedown', () => {
canvas.addEventListener('mousemove', draw)
canvas.addEventListener('mouseup', () => {
canvas.removeEventListener('mousemove',draw);
})
})
function draw(e) {
ctx.beginPath();
ctx.globalCompositeOperation = 'destination-out'
ctx.lineCap = 'round';
ctx.strokeStyle = '#fff'
ctx.lineWidth = '15';
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
canvas将画布导出为图像
canvas.toDataURL(type, encoderOptions)
。通过 canvas 身上的 toDataURL
方法,返回一个包含画布内容的 base64 格式的 data url。
参数:
- type:图片格式,默认为 image/png。
- encoderOptions:在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0-1 之间选择图片的质量。如果超出取值范围,将会使用默认值0.92。其他参数会被忽略。
ctx.fillStyle = 'red'
ctx.fillRect(50, 50, 100, 100)
const dataUrl = canvas.toDataURL()
// data:image/png;base64,iVBORw0KGgoAAAANSUh....
导出为Blob和ImageData数据的处理
有些场景下,你可能需要根据图片获取对应的ImageData数据或者Blob数据,传到后端做一些图像处理。这里就介绍下如何通过Canvas
获得这两种数据,并且互相转换
// 获取ImageData
function getImageData(url) {
return new Promise((res, rej) => {
//根据url创建出img对象来
const img = new Image(url);
img.src = url;
img.onload = () => {
//创建临时的canvas元素,用于获取imageData数据
const tempCanvas = document.createElement("canvas");
//将canvas设为和图片等大
tempCanvas.width = img.naturalWidth;
tempCanvas.height = img.naturalHeight;
//绘制图片,并获取imageData数据
const ctx = tempCanvas.getContext("2d");
ctx.drawImage(img, 0, 0);
res(ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height));
//tempCanvas.toBlob(res, type, quality)//获取Blob数据
}
img.onerror=(err)=>{
rej(err);
}
})
}
Canvas绘制的图形的事件处理
参考:canvas事件处理
Canvas是一个整体,图形本身实际都是Canvas的一部分,不可单独获取,所以也就无法直接给某个图形增加JavaScript事件。
给Canvas元素绑定事件
【基本思路】 ①给Canvas元素绑定事件addEventListener('click',()=>{},false)
,②当事件发生时,检查触发事件的位置(e.offsetX,e.offsetY
),③然后检查哪些图形覆盖了该位置isPointInPath
。要考虑这个判断过程的效率,有些还需要重新判断事件类型(click,mouseover,mouseenter,mousemove,mouseleave
),甚至要重新定义一个Canvas内部的捕获和冒泡机制。
但是由于isPointInPath
方法仅判断当前上下文环境中的路径,所以当Canvas里已经绘制了多个图形时,仅能以最后一个图形的上下文环境来判断事件。【解决方法】所以,当事件(click,mouseover,mouseenter,mousemove,mouseleave
)发生时,先清空画布clearRect
,开始重绘所有图形,每绘制一个就使用isPointInPath
方法,判断事件坐标(e.offsetX,e.offsetY
)是否在此次上下文环境中的图形覆盖范围内,根据冒泡机制,通过循环收集触发该事件的所有图形。
数组的最后一个成员处于Canvas最上层,而第一个成员则在最下层,我们可以视为最上层的成员是e.target,而其他成员则是冒泡过程中传递到的节点。当然这只是最简单的一种处理方法,如果真要模拟DOM处理,还要给图形设置父子级关系。
在实际运用时,如何缓存图形参数,如何进行循环重绘,以及如何处理事件冒泡,都还需要根据实际情况花一些心思去处理。另外,click是一个比较好处理的事件,相对麻烦的是mouseover、mouseout和mousemove这些事件,由于鼠标一旦进入Canvas元素,始终发生的都是mousemove事件,所以如果要给某个图形单独设置mouseover或mouseout,还需要记录鼠标移动的路线,给图形设置进出状态。由于处理的步骤变得复杂起来,必须对性能问题提高关注。
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
</head>
<body>
<canvas class="my-canvas" style="position: relative;" width="500" height="500"> </canvas>
<script>
let canvas = document.querySelector('.my-canvas');
let ctx = canvas.getContext('2d');
// ctx.beginPath();//重开个路径
// ctx.rect(10, 10, 100, 100);
// ctx.stroke();
// ctx.isPointInPath(20, 20); //true
// ctx.beginPath();//重开个路径
// ctx.rect(110, 110, 100, 100);
// ctx.stroke();
// ctx.isPointInPath(150, 150); //true
// ctx.isPointInPath(20, 20); //false,当Canvas里已经绘制了多个图形时,仅能以最后一个图形的上下文环境来判断事件,isPointInPath方法仅能识别当前上下文环境里的图形路径,而之前绘制的路径,无法回溯判断
arr = [
{ x: 10, y: 10, width: 100, height: 100 },
{ x: 110, y: 110, width: 100, height: 100 }
];
draw();
canvas.addEventListener('click', function (e) {
p = getEventPosition(e);
let who = draw(p);
console.log('who trigger', who);//获取canvas触发数组
}, false);
//获取触发元素的位置:
function getEventPosition(ev) {
var x, y;
if (ev.layerX || ev.layerX == 0) {
x = ev.layerX;
y = ev.layerY;
} else if (ev.offsetX || ev.offsetX == 0) { // Opera
x = ev.offsetX;
y = ev.offsetY;
}
return { x: x, y: y };
}
function draw(p) {
var who = [];
ctx.clearRect(0, 0, ctx.width, ctx.height);
arr.forEach(function (v, i) {
ctx.beginPath();
ctx.rect(v.x, v.y, v.width, v.height);
ctx.stroke();
if (p && ctx.isPointInPath(p.x, p.y)) {
//如果传入了事件坐标,就用isPointInPath判断一下
//如果当前环境覆盖了该坐标,就将当前环境的index值放到数组里
who.push(i);
}
});
//根据数组中的index值,可以到arr数组中找到相应的元素。
return who;
}
</script>
</body>
</html>