Canvas基础教程

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 结束。

  1. startAngle:圆弧的起始点,x 轴方向开始计算,单位以弧度表示。
  2. endAngle:圆弧的终点,单位以弧度表示。
  3. anticlockwise:true 表示逆时针,false 表示顺时针。(默认值)

贝塞尔曲线
二次贝塞尔曲线

ctx.quadraticCurveTo(cpx, cpy, x, y)

  1. cpx:控制点的 x 轴坐标。
  2. cpy:控制点的 y 轴坐标。
  3. x:终点的 x 轴坐标。
  4. 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):创建一个用于图像绘制使用的样式。

参数:

  1. image:图像源对象(规定使用的图像、画布或视频等)。
  2. repetition:重复图像的方式,值只能是 repeat | repeat-x | repeat-y | no-repeat。
  3. 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) 设置渐变颜色。

参数:

  1. position:介于 0-1 之间的值,表示渐变中开始与结束之间的位置。
  2. 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。

参数:

  1. type:图片格式,默认为 image/png。
  2. 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>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值